** Hooks是react@16.8版本新增的特性,他完全向后兼容,你可以任何时候在你的项目中引入Hooks新特性。Hooks让你的代码逻辑更收拢,更专注写状态处理过程,而不用将同一状态处理分散到各个生命周期内。此外,Hooks的提出,可以让函数组件拥有类组件的特性,使得组件性能提高,且代码可读性增强。 **
React Hooks之前class组件的写法
基于组件化的开发是当前前端项目中最基本的概念之一。在Hooks提出之前,React项目中写组件的两种形式分别为:函数式组件和Class类组件。函数组件就是一个函数,由于没有this,因此没有组件实例可言,从而更不具备状态和生命周期相关概念,只是拥有单纯的渲染UI的功能。
//下面是一个函数式组件🌰
function MyComponent(props) { //函数式组件没有状态,他的数据来源于父组件传递进来的props。在开发中,props参数一般处理成解构赋值,例如{name}
......//这里可以写一些处理数据的逻辑
return ( //函数式组件返回的DOM结构 (也就是组件UI)
<div>
<span>{props.name}</span>
</div>
)
}
//调用函数式组件
<MyComponent name="Wangyibo"/>
所以当你的组件需要处理状态逻辑时,就需要使用Class类组件。Class类组件不仅拥有State状态,还拥有生命周期。每个组件的实例,从创建、到运行、到销毁,在这个过程中,会发出一系列的事件,这些事件就叫做组件的生命周期函数。
由此可见,定义一个Class类组件,我们需要经历它的生命周期,并在不同的生命周期进行不同的操作。
//下面是一个Class类组件🌰
import React from 'react'
import ReactDOM from 'react-dom';
class Counter extends React.Component {
static defaultProps = {
//设置props的默认值
name: 'Wangyibo',
age:23
};
//调用构造函数
constructor(props) {
super(props); //接受到父组件传入进来的props
//初始化state
this.state = {number: 0}
}
componentWillMount() {
console.log('父组件挂载之前');
}
componentDidMount() {
console.log('父组件挂载完成');
}
shouldComponentUpdate(newProps, newState) {
console.log('父组件是否需要更新');
if (newState.number<15) return true; //true 表示需要更新
return false //false表示不需要更新
}
componentWillUpdate() {
console.log('父组件将要更新');
}
componentDidUpdate() {
console.log('父组件更新完成');
}
handleClick = () => { //使用箭头函数,利用箭头函数的this特性,这里就不需要在constructor里面去给函数绑定this了
this.setState({
number: this.state.number + 1
})
};
render() {
console.log('render(父组件挂载)');
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
{ this.state.number < 10 ?
(此处可渲染其他子组件)
:null}
</div>
)
}
}
ReactDOM.render(<Counter/>, document.getElementById('root'));
class组件完美的体现了面向对象编程的思想——先有一个实例对象、然后再有属性和方法。也就是说,我们需要先拿到this对象,然后在为这个this定义属性和方法。在整个组件的书写过程中,我们都要通过这个this组件实例去获取它的数据和方法。 此外,class组件的生命周期定义,我们需要将获取状态的逻辑遍布在适合的生命周期函数中, 由于生命周期的独立性,我们不能很好地复用状态逻辑,同时,生命周期的定义导致我们对状态的处理也是呈周期性的:当我们的业务复杂,状态变多时,同一生命周期函数中会有处理不同状态的逻辑,处理同一状态的逻辑会遍布在不同生命周期中,从而导致我们的代码阅读性很差,逻辑性也很松散。
时代在变迁,框架也需要提升。每次编写组件都需要先判断是有状态还是无状态,从而决定是选择函数组件还是类组件。如果需要给无状态组件添加状态,还需将无状态组件重新改写为class类组件,并且class类组件都需要写这么一套模板,代码书写比较繁琐。为此,针对class组件编写代码繁琐、状态逻辑不易复用、状态逻辑处理分散、有无状态组件切换编写等问题,React在16.8版本中提出了Hooks的概念。
React Hooks新世界
在React 16.8版本中,编写组件的方式采取最简单性能更好的函数组件形式,由于函数组件没有this实例的概念,因此提出Hooks来让其拥有了组件状态,打破了我们无法在函数组件中拥有状态的困境。React Hooks的提出,也打破了传统的面向对象封装的Class组件的概念,不在做什么事都需要this指针去取数据或方法了,而是以函数闭包的形式,直接在组件的全局引擎中执行。因此,在使用hooks书写组件的过程中,我们应该忘记生命周期函数,忘记操作state属性的统一方法,而应拥抱hooks的全局执行函数。
与 Class 类组件相比,使用hooks有以下几点优点:
- 组件编写一致。统一使用函数组件的形式编写组件,而不需要在class组件和函数组件之中切换与更改。
- 代码量减少。不在需要编写class那一整套繁琐的组件模板,不需通过this来获取数据和方法,也不需要额外的为函数绑定this对象。
- 逻辑紧密。相同的状态逻辑可以写在一起,而不需要分散到各个生命周期函数里了。
- 易于复用。在class组件中,一般使用HOC或render props的形式来复用组件逻辑,却对于生命周期相关的逻辑却没办法抽象。但是在hooks中可以很方便的封装状态逻辑,易于复用。
为解决Class组件存在的问题,顺利的从class过渡到hooks,React Hooks提出以下hooks 函数:
下面将对各个hooks进行详细阐述。
useState
为解决函数组件没有状态的问题,React hooks 提出了 useState 来让函数组件拥有状态。
用法:
const [state,setState] = useState(initState)
useState()返回一个数组,数组的元素分别是一个state和一个更新state的函数。在函数组件内的任何地方,都可以直接调用state,直接更改state。initState可以是多种数据类型,包括字符串、布尔值、对象或者函数。
const getDefaultCheckVals = () => {..}
const [defaultCheckVals,setDefaultCheckVals] = useState(getDefaultCheckVals)
如果想在render之前执行的获取状态的函数 (也就是初始化的时候去获取状态默认值),可以写在useState里面。
使用对比
在Class组件中,设置与读取state的方式为:
class A extents React.component {
// class组件中,state类型是一个对象,以key:value的形式设置初始state
this.state = {
state1: value1,
state2: value2,
...
}
...
//获取state,例如
this.state.state1
...
//设置state,例如
this.setState({
state1: newValue1
})
}
采用 useState hook,在函数组件中设置state的方式为:
function A(){
const [state1,setState1] = useState(0)
const [state2,setState2] = useState([])
...
//获取state,例如
state1 //直接获取
...
//设置state,例如
setState1(2)
}
对比class组件和函数式组件中的state的写法, useState定义的state可以在组件全局中直接调用,不需要用过this去获取;其次,useState的state类型可以是任意类型,而class中state只能是对象。
踩坑
🌰1 : 组件重新渲染时,为什么给useState传递新参数不生效?
const Child = ({name}) => {
const [name,setName] = useState(name)
return (
<div>
name: {name}
</div>
)
}
const parent = () => {
const [name,setName] = useState('WYB')
return (
<div>
<button onClick={()=>{setName('Wangyibo')}}>修改name</button>
<Child name={name} />
</div>
)
}
问题:
发现点击修改name,页面中的name还是没有更新。调试发现Child组件是收到了这个更新后的props的。
😁解答:
回想起了React文档说的:在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state,而不再是传入的参数值。
🌰2 :修改state内容,为什么组件不重新渲染?
const Child = ({maleGod:{name,age}}) => {
console.log('渲染了')
return (
<div>
name: {name}
age: {age}
</div>
)
}
const test = () => {
const [maleGod,setMaleGod] = useState({name:'WYB',age:23})
return (
<div>
<button onClick={()=>{setMaleGod(maleGod)}}>是他是他就是他</button>
<Child maleGod={maleGod}/>
</div>
)
}
问题:
每次重新设置了state的值,但是页面中内容没有变化。
😁解答:
对于函数组件,只要props或state更改了,组件就会重新渲染。 但是,是要真正的更改,如果你set state的值和上次一样或者对象的引用地址没变,则组件是不会重新渲染的。一般来说,容易踩坑的地方就是在处理对象上,就好比[]===[]永远为false一样,在设置对象类型的state时,一定要记得赋值的是一个新对象。通常的方式有两种:
//方式一,采用es6的对象解构
{...obj}
//方式二,采用Object.assign(),复制新对象
Object.assign({},obj,{...}
对上面的🌰进行修改如下:
...
<button onClick={()=>{setMaleGod({...maleGod})}}>是他是他就是他</button>
...
优化
比如前面的例子:
const getDefaultCheckVals = () => {..}
const [defaultCheckVals,setDefaultCheckVals] = useState(getDefaultCheckVals())
上面的代码中,每次组件重新渲染,getDefaultVals函数都会执行一次。 因此,为了让这个设置默认值的函数只在最初执行一次,可以以回调函数的形式进行改写:
const getDefaultCheckVals = () => {..}
const [defaultCheckVals,setDefaultCheckVals] = useState(()=>getDefaultCheckVals())
useEffect
为解决在Class组件中很难复用生命周期函数里面的逻辑、并且处理相同状态的逻辑被分散到了多个生命周期函数中的问题,React hooks 提出了 useEffect 来让函数组件可以处理原本在生命周期中的副作用。
用法:
const A = ()=>{
const showLog = ()=>{
console.log('myLog')
}
useEffect(()=>{
docuemnt.getElementByID('btn').addEventListener('click',showLog)
return ()=>{
docuemnt.getElementByID('btn').removeEventListener('click',showLog)
}
},[])
return (
<button id='btn'>button</button >
)
}
useEffect() 接收两个参数:第一个必填参数是funciton, 用于处理dom节点挂载好之后执行的副作用,完成了componentDidMount 和 componentDidUpdate 函数中执行的副作用。第二个可选参数是一个数组,表示依赖项——当数组中的依赖项改变了,useEffect中的副作用会重新执行,如果不传任何依赖项,即空数组,表示useEffect依赖项永远不会改变,则useEffect中的副作用只在首次组件render的时候才会执行。当不传第二个参数,表示没有依赖项,useEffect中的副作用在组件每次render的时候都会执行。 (依赖项是用Object.js()进行的浅比较)
使用对比:
以上面用法中的示例为例,在class中使用生命周期函数处理副作用如下:
class A extents React.component {
...
componentDidMount(){
docuemnt.getElementByID('btn').addEventListener('click',showLog)
}
componentDidUnMount(){
docuemnt.getElementByID('btn').removeEventListener('click',showLog)
}
...
}
对比class组件和函数式组件中的副作用的写法, 相比于class的生命周期分散业务逻辑,useEffect可以让业务逻辑更加聚集,代码的可阅读性和可维护性性更好。当业务更加复杂时,useEffect的写法还好让代码变得更简洁。
踩坑:
🌰1:state变更触发重新渲染,导致重新定义新变量,从而触发useEffect无限循环。
const test = ()=>{
const [lists,setLists] = useState([])
const defaultValue = Object //这里表示defaultValue数据的值是Object类型
useEffect(()=>{
doSomeThings() // 这里面拥有处理状态的业务逻辑。
},[defaultValue])
}
问题:
为什么doSomething() 进入了无线循环执行? (无限)
😁解答:
由于state的改变会触发组件重新渲染,对于函数组件来说,重新渲染就是整个函数重新执行,因此会导致defaultValue会重新定义为新的对象实例,由于新对象肯定不相等(引用地址变化了),所以导致useEffect 因为依赖项变更而执行里面的副作用,副作用的执行又会更改state,从而导致进入了无线循环。 因此,在上述类似场景中,我们可以将依赖项修改为某个具体的属性值,或者在useEffect里面通过if条件对依赖项做进一步判断,从而决定是否需要执行doSomeThings()
🌰2:useEffect 闭包导致state值不变
const test = ()=>{
const [count,setCount] = useState(0)
useEffect(()=>{
const timer = setInterval(()=>{
setCount(count+1)
},1000)
return ()=>clearInterval(timer)
},[])
return (
<div>{count}</div>
)
}
问题:
定时器里修改count的值没有生效,一直显示1.
😁解答:
由于useEffect没有依赖项,那么useEffect里面的副作用只在组件初次渲染时执行一次,此时useEffect里面的状态都是初始值。又由于useEffect闭包的原因,定时器每次执行取得count都是初始值0,所以最终页面展示的一直为1。
//有以下两种修改代码方式可以得到正确的结果
//方式一 以回调函数的形式修改state。
//将状态维护在自己的函数局部作用内,每次调用setCount时是直接获取自己内部的状态,因此结果符合预期。
...
useEffect(()=>{
const timer = setInterval(()=>{
setCount( count => count+1 )
},1000)
return ()=>clearInterval(timer)
},[])
...
--------------------------------------------------------------------------------
//方式二 将状态变量添加至依赖项
// 依赖项状态改变,每次副作用里面取到的状态都是最新值。
...
useEffect(()=>{
const timer = setInterval(()=>{
setCount(count+1)
},1000)
return ()=>clearInterval(timer)
},[count])
...
优化:
useEffect的优化主要是在定义useEffect副作用时,根据定义进行优化:
- 在卸载组件时,将有必要清除的副作用进行清除,例如绑定的事件监听、定时器等等;
- 为了防止组件每次渲染都执行副作用,只要传递第二个依赖项数组参数即可,注意确保依赖项数组包含了副作用函数中会随时间变化并且在副作用中使用的变量;
- 为了很明确你的副作用effect包含了哪些变量,可以将外部定义的业务处理函数移动到effect函数内定义;
- 不依赖状态、不依赖内部变量的函数,可以抽离到组件外封装成工具函数;
- 依赖内部变量的函数,可以抽离到effect外面,让effect依赖它的返回值;
- 如果实在是逻辑太复杂了,可以将函数添加进effect的依赖项,但是注意将函数用useCallback 包起来,防止每次渲染组件都调用Effect。
- 最好不要用对象类型的数据作为依赖项,防止数据相同只是引用地址不同,而导致的重复渲染问题;
目前为止,useState 和 useEffect两个hooks:一个处理状态,一个替代生命周期函数,基本上就可以完成绝大多数业务项目开发。但是,为了减少组件重新渲染重新计算带来的应用性能问题、为了提升代码的阅读性和减少组件props层层传递的问题,在项目开发中,还应灵活选择合适的hooks,优化组件性能、减少props嵌套传递问题。
useLayoutEffect
useLayoutEffect也是一个hook方法,从名字上看和 useEffect 差不多,他俩用法也比较像。但是在90%的场景下我们都会用 useEffect,然而在某些场景下却不得不用 useLayoutEffect(比如需要同步调用一些副作用,则可以在DOM更新之后同步执行)。useEffect 和 useLayoutEffect 的区别是:
- useEffect 不会 block 浏览器渲染,而 useLayoutEffect 会。
- useEffect 会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行。 所以尽可能使用标准的useEffect以避免阻塞视觉更新。
useCallback
useCallback 用于避免不必要的渲染,主要用于缓存函数。一般在子组件调用父组件的方法时,这个方法都用useCallback包起来。
用法:
...
const handleSearch = useCallback(()=>{
// 一系列业务逻辑
})
...
<Child handleSearch={handleSearch}/>
useCallback() 接收两个参数:第一个必填参数是定义的回调函数,用于组件的事件处理,它在组件的渲染过程中,只是定义,并不运行;第二个可选参数是一个依赖项数组,用于控制该回调函数是否需要重新定义。
💡注意💡:当里面的回调函数用到了状态数据时,一定要记得将该状态变量添加进依赖项里,使得状态变更,重新以最新状态值更新回调函数,否则回调函数里面的状态变量永远是第一次渲染时的初始值。
优化组件示例:
// test1 :简答粗暴的传一个箭头函数
const test1 = ()=>{
...
return (
<Button onClick={()=>{...}} />
)
}
// 问题:test1 代码中,直接将业务逻辑以匿名函数的形式传给了Button,先不说业务逻辑与UI耦合,这种形式还存在一个问题就是当test组件每次重新渲染的时候,都会生成一个新的重复的匿名箭头函数,导致Button组件的 onClick 属性变更,从而触发Button组件的重新渲染 (渲染结果没有变化)
--------------------------------------------------------------------------------
// test2 : 解耦业务逻辑和UI组件
const test2 = ()=>{
const [count,setCount] = useState(0)
const [initVal, setInitVal] = useSatte('')
const onChange = (e) => {
setInitVal(e.target.value)
}
const addCount = () => {
setCount(count+1)
}
return (
<Input onChange={onChange} value={initVal}/>
<button onClick={addCount}>点我+1: {count}</button>
)
}
// 问题:test2代码中,将业务逻辑抽离出来了,传递引用地址给组件。但是点击button组件时,由于更改了count state的值,会导致组件test2重新渲染,函数组件的重新渲染就是整个函数的重新执行,因此又重新定义了新的 onChange 函数,导致Input组件的onChange属性变化了 (因为函数是对象类型,每次新定义的函数引用地址是不一样的),从而导致Input组件做了不必要的渲染(因为点击button,只是更改了count的值,与Input相关的props都没有改变)
--------------------------------------------------------------------------------
// test3 : 体现useCalllback价值的地方来了
const test3 = ()=>{
const [count,setCount] = useState(0)
const [initVal, setInitVal] = useSatte('')
const onChange = useCallback((e) => {
setInitVal(e.target.value)
},[]) //如果不传递参数,则组件每次渲染也都会重新定义回调函数。又因为该回调函数不依赖状态,所以只传递空数组依赖即可,仅在组件初次渲染时定义回调函数
const addCount = useCallback(() => {
setCount(count+1)
},[count]) // 因为回调函数中用到了状态变量,所以需要将该状态作为依赖项传递,使得每次回调拿到的状态都是最新值,而不是初次渲染时的初始值。
return (
<Input onChange={onChange} value={initVal}/>
<button onClick={addCount}>点我+1: {count}</button>
)
}
// 优势:用了useCallback包裹回调函数后,则不会因为父组件的重新渲染导致子组件不必要的渲染,从而减少渲染,提升组件性能。
😺其他:一般与useCallback配合使用的还有React.memo(),该函数用来缓存组件,防止组件仅仅因为某个props的引用地址变化而带来的不必要的渲染。(这里进行的是浅比较,并且比较的是整个props对象)。如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现 (该函数接收两个参数:prevProps,nextprops。 返回true,不重新渲染;返回false,则重新渲染)
因为props或state的改变,会导致组件重新渲染; 而父组件的渲染,因为是函数组件,所以整个函数都要重新执行一次,使得子组件也会重新执行一次,但是子组件的props并没有变化,那么子组件就做了不必要的重新执行,所以这里可以用React.memo把组件包起来,减少子组件不必要的更新。 此外,如果子组件的props包含了父组件的方法,则会由于父组件的重新渲染生成新的对象实例,导致引用地址变化,从而导致子组件更新,所以这里需要用useCallback将这个方法包起来,减少不必要的方法重新定义,从而减少组件的重新渲染。
useMemo
useMemo 用于避免不必要的重复计算,主要用于缓存一些复杂的计算结果,防止每次渲染组件都要重新执行这个复杂的计算过程,类似于Vue里面的计算属性,它在组件渲染过程中直接执行。
用法:
const sum = useMemo(()=>{
// 一系列计算
},[count])
useMemo() 接收两个参数:第一个必填参数是一个函数,用于执行相应的计算,返回计算后的新值,这个值将被缓存起来 (也可以没有返回值,单纯的缓存某个函数的运行过程,依赖项不改变,这个函数就不会重新运行,适合处理高阶函数的场景); 第二个可选参数是一个依赖项数组,只要依赖项有值改变了,这个函数就会重新执行。如果没有提供这个依赖项,useMemo在每次渲染时都会计算新的值。
优化组件示例:
const App = () => {
const [count1,setCount1] = useState(0)
const [message,setMessage] = useState('abcdefg')
useEffect(()=>{
const timer = setInterval(()=>{
setCount1((count1)=>count1+1) // 每秒都要更新一次count1,那么组件也是每秒重新渲染一次
},1000)
return ()=>clearInterval(timer)
},[])
// 定义一个用于反转字符串的函数
const reverse = () => {
console.log(message)
return message.split('').reverse().join('')
}
// 获得反转字符串。由于前面定义的定时器,导致组件每秒都要重新渲染一次,因此该计算反转字符串的函数也是每秒都会重新调用执行一次。如果计算量很大,则每次都要执行是很消耗性能的。
const reversedMessage = reverse()
const doChangeStr = ()=>{
setMessage(message=>message+Math.ceil(Math.random()*10))
}
return (
<div className="App">
<div>{count1}</div>
<button onClick={doChangeStr}>修改字符串</button>
<p>原始字符串{ message }</p>
<p>计算后反转字符串: { reversedMessage }</p>
</div>
);
}
--------------------------------------------------------------------------------
// 体现useMemo价值的地方来了
const App = () => {
const [count1,setCount1] = useState(0)
const [message,setMessage] = useState('abcdefg')
useEffect(()=>{
const timer = setInterval(()=>{
setCount1((count1)=>count1+1)
},1000)
return ()=>clearInterval(timer)
},[])
// 组件初次渲染时计算得到reversedMessage,然后后面组件的每秒刷新,reversedMessage这个值都是从缓存中读取,而不会重新计算得到,只有当依赖项message改变才会重新计算reversedMessage的值
const reversedMessage = useMemo(()=>{
console.log(message)
return message.split('').reverse().join('')
},[message])
const doChangeStr = ()=>{
setMessage(message=>message+Math.ceil(Math.random()*10))
}
return (
<div className="App">
<div>{count1}</div>
<button onClick={doChangeStr}>修改字符串</button>
<p>原始字符串{ message }</p>
<p>计算后反转字符串: { reversedMessage }</p>
</div>
);
}
useReducer
useReducer是useState的替代方案,用法也和useState很相似。useState是基础的用于定义初始state的hooks,但是在业务比较复杂,某个state的子状态比较多 (action比较多:改变state的场景比较多)的情况下,useReducer会比useState更适用。
用法:
const [state,dispatch] = useReducer(reducer,initState,init)
类似于redux的工作规则,改变state的规则只有触发action对象,由reducer接收这个action对象,再根据action的类型进行更新state。因此,useReducer()接收两个参数:第一个必填参数是一个reducer函数,形如 (state, action) => newState,第二个必填参数是state的初始值。返回值是一个数组,数组的第一项是state,第二项是一个dispatch调度函数,用于触发action。第三个参数是一个可选值,用来惰性提供初始状态,我们可以使用一个init函数来计算初始状态/值,而不是显示的提供值。需要将init函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为init(initState),如果初始值可能会不一样,这会很方便,最后会用计算的值来代替初始值。
优化组件示例:
const UseReducerTest = () => {
const [val, setVal] = useState(0)
const doAdd = ()=> setVal(val+1)
const doSub = ()=> setVal(val-1)
const doMul = ()=> setVal(val*2)
const doDiv = ()=> setVal(val/3)
return (
<div>
<button onClick={doAdd}>+</button>
<button onClick={doSub}>-</button>
<button onClick={doMul}>x</button>
<button onClick={doDiv}>/</button>
当前val为:{val}
</div>
)
}
// useState 处理状态是完全没有问题的,只是当业务逻辑很复杂、修改状态的action比较多时,采用useReducer处理更优。useReducer对状态进行集中管理,更易对状态进行维护,并且业务逻辑更集中。
--------------------------------------------------------------------------------
const reducer = (state,action)=>{
switch (action.type) {
case 'addition':
return state+1;
case 'subtraction':
return state-1;
case 'multiplication':
return state*2
case 'division':
return state/3;
default:
return state;
}
}
const UseReducerTest = () => {
const [val, dispatch] = useReducer(reducer,0)
return (
<div>
<button onClick={()=>{dispatch({type:'addition'})}}>+</button>
<button onClick={()=>{dispatch({type:'subtraction'})}}>-</button>
<button onClick={()=>{dispatch({type:'multiplication'})}}>x</button>
<button onClick={()=>{dispatch({type:'division'})}}>/</button>
当前val为:{val}
</div>
)
}
useContext
useContext主要是用于组件之间共享全局状态的一种技术(跨组件传值),减少组件传值时的层层嵌套问题,且避免因props改变带来的不必要的渲染。当它提供的值变更时,他包含的所有子级组件都会重新渲染,这是没有办法避免的。Context 只放置必要的,被大多数组件所共享的状态。对非常昂贵的组件,建议在父级获取 Context 数据,通过 props 传递进来
用法:
const value = useContext(MyContext)
useContext接收一个context对象,这个对象由React.creatContext创建,并返回该context的当前值。在组件中使用时,组件所取的context值由上层组件中距离当前组件最近的<MyContext.Provider>的value prop决定。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染
优化组件示例:
const Content = (props) => {
return (
<div>
<span>
{props.text}
</span>
</div>
)
}
const Child = (props) => {
return (
<div>
<Content text={props.text} />
</div>
)
}
const UseContextTest = () => {
const [text, setText] = useState('hello');
return (
<div>
<Child text={text} />
</div>
)
}
// 在跨组件传值时,都是采用props的方式层层传递下去,直到到了目标组件。这个过程中出了繁琐的嵌套地狱,还会因为props的改变,导致组件树上的每一个组件重新渲染。
--------------------------------------------------------------------------------
const dataOrigin = {
text:'hello'
}
const ShareData = React.createContext(dataOrigin)
const Content = () => {
const data = useContext(ShareData)
return (
<div>
<span>
{data.text}
</span>
</div>
)
}
const Child = () => {
return (
<div>
<Content/>
</div>
)
}
const UseContextTest = ()=>{
return (
<ShareData.Provider value={{text:'hahaha'}}>
<Child/>
</ShareData.Provider>
)
}
useRef
ref主要用于获取DOM节点(对DOM对象的引用,是真正的引用,而不是值拷贝),在父组件中通过ref来反向调用子组件的内部状态或方法。useRef() 是获取ref的一个方法,只能用于函数组件。useRef返回一个可变的ref对象,但是该对象在组件的整个生命周期内保持不变。它拥有一个current属性,该属性用于保存ref对象的可变值(状态或方法),也就是说React会将ref对象的.current属性设置为相应的DOM节点。
用法:
const tableRef = useRef()
一般可用ref来操作同辈或子组件。
官网的示例:
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>
</>
);}
useImperactiveHandle
主要用于透传ref,然后可以在父组件中获取子组件的引用。一般情况下避免直接使用ref这样的命令式代码,而是与forwardRef一起使用,从而可自定义暴露给父组件的实例值。目前在项目中
示例用法:
const { useCallback } = require("react")
const Child = forwardRef(({ prod1,prod2 }, ref) => {
...
useImperativeHandle(ref, () => ({
refresh: () => {
// doSomethings
...
}
}))
...
}
const parent = ()=>{
const childRef = useRef()
const refreshTable = useCallback(()=>{
childRef?.current?.refresh()
},[])
...
return (
<div>
<Button onClick={refreshTable}/>
<Child ref={childRef}>
</div>
)
}