1、hook使用
1.1、useReducer
useReducer仅仅是useState的一种替代方案
- 在某些情况下,如果state 的处理逻辑比较复杂,我们可以通过
useReducer来对其进行拆分; - 或者这次修改的state需要依赖之前的state,也可以使用; 单独创建一个reducer/counter.js文件:
export funxtion conterReducer(state, action) {
switch(action.type) {
case 'increment':
return {...state, counter: state.counter + 1}
case 'decrement':
return {...state, counter: state.counter - 1}
default:
return state;
}
}
home.js
import React, {useResucer} from 'react'
import {counterReducer} from './reducer/counter'
export default function Profile() {
const [state, dispatch] = useReducer(CounterReducer, {counter: 0})
return <>
<h2>当前计数:{state.counter}</h2>
<button onclick={e => dispatch({type: 'increment'})}> 加 </button>
<button onclick={e => dispatch({type: 'decrement'})}> 减 </button>
</>
再来看一下,创建另外一个profile.js也适用这个reducer函数,是否会进行数据的共享:
import React, {useReducer} from 'react'
import {counterReducer} from './reducer/counter'
export default function Profile() {
const [state, dsipatch] = useReducer(counterReducer, {counter: 0})
return <>
<h2> 当前计数:{state.counter}</h2>
<button onclick={e => dispatch({type: 'increment'})}> 加 </button>
<button onclick={e => dispatch({type: 'decrement'})}> 减 <button>
</>
数据是不会共享的,它们只是共用了counterReducer的函数而已,所以useReducer只是useState的一种替代,而不是redux的替代
1.2、useCallback
useCallback实际的目的是为了进行性能的优化。 如何进行性能的优化呢?
useCallback会返回一个函数的memorized(记忆的)值;- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const memorizedCallback = useCallback( () => {
doSomething(a,b)
}, [a, b])
例如下面的代码:
import React, {memo, useState, useCallback} from 'react'
export default function CallbackHookDemo() {
const [count, setCount] = useSate(0)
const increment1 = useCallback(() => {
setCount(count + 1)
}, []);
const increment2 = function() {
setCount(count + 1)
}
return <>
<h2> 当前计数:{count} </h2>
<button onclick={increment1}>加 </button>
<button onclick={increment2}> 减 </button>
</>
- increment1在每次组件重新渲染的时候,会返回相同的值;
- increment2每次定义的都是不同的值;
- 问题:是否是increment1会比increment2更加节省性能呢?
- 事实上,经过一些测试,并没有更加节省内存,因为useCallback中还会传入一个函数作为参数;
- 所以并不存在increment2每次创建新的函数,而increment1不需要创建新的函数这种性能优化;
- 那么,为什么说useCallback是为了进行性能优化?
所以我们对上面的代码进行改造:
import React, { memo, useState, useCallback } from 'react';
const CounterIncrement = memo((props) => {
console.log("CounterIncrment被渲染:", props.name);
return <button onClick={props.increment}>+1</button>
})
export default function CallbackHookDemo() {
const [count, setCount] = useState(0);
const increment1 = useCallback(function increment() {
setCount(count + 1);
}, []);
const increment2 = function() {
setCount(count + 1);
}
return <div>
<h2>当前计数: {count}</h2>
{/* <button onClick={increment1}>+1</button>
<button onClick={increment2}>+1</button> */}
<CounterIncrement increment={increment1} name="increment1"/>
<CounterIncrement increment={increment2} name="increment2"/>
</div>
)
}
在上面的代码中,我们将回调函数传递给了子组件,在子组件中会进行调用;
- 在点击按钮时,我们发现在接受increment1,的子组件不会重新渲染,但是接受increment2的子组件会重新渲染;
- 所以useCallback最主要作用于性能渲染的地方应该是和memo结合起来,决定子组件是否需要重新渲染;
1.3、useMemo
useMemo实际的目的也是为了进行性能优化。
如何进行性能优化呢?
- useMemo返回的也是以一个memorized的值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
const memorizedValue = useMemo(() => computedExpensiveValue(a, b), [a, b]);
例如:
- 我们无论点击加1的按钮还是切换的按钮,值都会重新计算一次;
- 事实上,我们只是希望在count发生改变的时候才去重新计算;
import React, {useState, useMemo} from 'react'
function caculateNum(count) {
let total = 0;
for(let i = 0; i < count; i++) {
total += i;
}
console.log('计算一遍');
return total;
}
export default function MemoHookDemo() {
const [count, setCount] = useState(0);
const [isLogin, setLogin] = useState(true);
const total = caculateNum(count);
return <>
<h2>total的值为:{total}</h2>
<button onClick={e => setcount(count + 1)}>加1</button>
<h2>{isLogin ? '登录' : '注销'}</h2>
<button onClick={e => setLogin(!isLogin)}>切换</button>
</>
}
下面代码,我们用useMemo来进行优化:
import React,{useState, useMemo} from 'react';
function caculateNum(count) {
let total = 0;
for (let i = 0; i < count; i++} {
total += i;
}
console.log('计算次数');
return total;
}
export default function MemoHookDemo() {
const [count, setCount] = useSate(10);
const [isLogin, setLogin] = useState(true);
const total = useMemo(() => {
return calculateNum(count);
}, [count]);
return <>
<h2> total的值为: {total}</h2>
<button onClick={e => setCount(count + 1)}> 加1 </button>
<h2>{isLogin ? '登录' : '注销'}</h2>
<button onClick={e => setLogin(!isLogin)}> 切换 </button>
</>
}
当然useMemo也可以用于子组件的优化
例如:
import React, {useState, useMemo, memo} from 'react'
function caculateNum(count) {
let total = 0;
for (let i = 0; i < count; i++) {
total += i;
}
console.log('计算一次');
return total;
}
const ShowCouter = memo((props) => {
console.log('showCounter组件重新渲染')
return <h1> counter 的值是: {props.total}</h1>
})
const ShowInfo = memo((props) => {
console.log('showInfo组件重新渲染')
return <h1> 父组件的信息:{props.info.name} </h1>
})
export default function MemoHookDemo() {
const [count, setCount] = useState(10);
const [isLogin, setLogin] = useState(true);
const total = useMemo(() => {
return caculateNum(count);
}, [count]);
const info = useMemo(() => {
return {name: 'xxx'}
}, []);
return <>
<h2> total的值:{total} </h2>
<ShowCounter total={total} />
<ShowInfo info={info} />
<button onClick={e => setCount(count + 1)}> 加1 </button>
<h2>{isLogin ? '登录' : '注销'}</h2>
<button onClick={e => setLogin(!isLogin)}> 切换 </button>
</>
从上述代码看出,ShowCounter子组件依赖的是一个基本数据类型,所以在比较的时候只要值不变,那么就不会重新渲染;ShowInfo子组件接收的是一个对象,每次都会定义一个新的对象,所以我们需要通过useMemo来进行优化;
1.4、useRef
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
最常用的ref是两种用法:
- 用法一:引用DOM(或组件,但是,是class组件)元素;
import React, {useRef} from 'react';
export default function UseRefHookDemo() {
const inputRef = useRef();
const titleRef = useRef();
const handleDom = () => {
titleRef.current.innerHtml = '我是xxx';
inputRef.current.focus();
}
return <>
<h2 ref={titleRef}>我是标题dom</h2>
<input type="tyxt" ref={inputRef} />
<button onClick={e => handleDom()}>操作Dom<button>
</>
- 用法二:保存一个数据,这个对象在整个生命周期中可以保持不变; 例如: 使用Ref保存上一次的某一个值
import React, {useRef} from 'react'
export default function UseRefHookDemo() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
return <>
<h2>前一次的值:{countRef.current}</h2>
<h2>这一次的值:{count}</h2>
<button onclick={e => setCount(count + 1)}>加1</button>
</>
useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的
1.5、useImperativeHandle
useImperativeHandle,这个hook可以将函数内的方法返回给ref绑定的那个组件;
先来回顾一下ref和forwardRef结合使用:
- 通过
forwardRef可以将ref转发到子组件; - 子组件拿到父组件中创建的
ref,绑定到自己的某一个元素中; - 先定义class类组件
Hello.js
import React, {Component} from 'react'
export default calss Hello extends Component {
handler = () => {
alert(1)
}
render() {
return (<div>hello react hooks!</div>)
}
}
App.js
import React, {Component} from 'react'
import Hello from './Hello'
export default class App extends Component {
clickFn = () => {
this.hello.handler() //触发子组件的方法
}
render() {
return <>
<button onClick={this.clickFn}>点击</button>
<Hello ref={el => this.hello = el} />
</>
}
}
- 函数组件
import React, { useRef, forwardRef } from 'react';
const HYInput = forwardRef(function (props, ref) {
return <input type="text" ref={ref}/>
})
export default function ForwardDemo() {
const inputRef = useRef();
return (
<div>
<HYInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>聚焦</button>
</div>
)
}
- 以上的做法是,把子组件的dom直接暴露给了父组件
- 带来的问题:
-
- 会带来一些不可控的情况;
-
- 父组件可以拿到dom后进行任意的操作;
-
- 实际上,我们只希望父组件只能操作
add方法,其他并不希望随意被操作;
- 实际上,我们只希望父组件只能操作
- 使用
useImperativeHandle可以只暴露固定的操作 App.js
import React, {useRef} from 'react'
import Hello from './Hello'
export default function App(){
const helloRef = useRef(null)
const handler = () => {
//拿到hello组件内部的方法
console.log('hello', helloRef)
helloRef.current.add()
}
return <>
<button onClick={handler}>触发子组件Hello中的方法</button>
<Hello ref={helloRef} />
</>
}
Hello.js
import React, {forwardRef, useImperativeHandle, useState} from 'react'
//高阶组件
export default forwardRef(function Hello(props, ref) {
const [count, setCount] = useState(0)
//这里定义方法
useImperativeHandle(ref, () => {
//这里定义我们想让父组件执行的方法
return {
add() {
setCount(count => count ++)
}
}
})
const handler = () => {
alert(1)
}
render() {
return (<div>hello react hooks!</div>)
}
})
1.6、useLayoutEffect
useLayoutEffect和useEffect很相似,但是有区别;
useEffect会在,渲染的内容更新到Dom上后执行,不会阻塞dom的更新;useLayoutEffect会在,渲染的内容更新到DOM上之前执行,会阻塞dom的更新; 如果我们希望在某些操作发生之后再更新Dom,那么应该将这个操作放到useLayoutEffect例如:
import React, {useEffect, useState, useLayoutEffect} from 'react';
export default function EffectHookDemo() {
const [count, setCount] = useState(0);
useEffect(() => {
if(count === 0) {
setCount(100)
}
}, [count])
return <>
<h2> 当前数字:{count}</h2>
<button onClick={e => setCount(0)}>随机数</button>
</>
例如,上述代码,会有闪烁的现象;
- 因为我们先设置了count为0,dom就会被更新一次,并且会执行一次useEffect中的回调函数,再次执行setCount操作, 那么dom会再次被更新,并且useEffect又会被执行一次;
- 实际上,我们希望的是,,count被设置为0时,随机另外一个数字;
使用useLayoutEffect
import React, {useEffect, useState, useLayoutEffect} from 'react'
export default function EffectHookDemo() {
const [count, setCount] = useState(0)
useLayoutEffect(() => {
if (count === 0) setCount(100)
}, [count])
return <>
<h2> 当前count: {count} </h2>
<button onClick={e => setCount(0)}>点击</button>
</>
2、自定义hook
2.1、自定义hook的解释
- 自定义hook只是一种函数代码逻辑的抽取。
例如:所有的组件再创建和销毁时,都进行打印
- 组件被创建: 打印‘组件被创建’
- 组件被销毁: 打印‘组件被销毁’
export default function Home() {
useEffect(() => {
console.log('组件被创建');
return () => {
console.log('组件被销毁');
}
}, [])
return <>
<h2>CustomHookDemo</h2>
</>
}
export default function Login() {
useEffect(() => {
console.log('组件被创建');
return () => {
console.log('组件被销毁');
}
}, [])
return <>
<h2>CustomHookDemo</h2>
</>
}
- 但是这样做,就是需要所有组件都需要有对应的逻辑;
- 如何对他们的逻辑进行抽取到一个函数中?
function commonMethods() {
useEffect(() => {
console.log('组件被创建');
return () => {
console.log('组件被销毁');
}
})
}
- 但是抽取之后,代码是报错的;原因是普通的函数中不能使用hook;
- 那如何操作呢?
- 函数以特殊的方式命名,以
use开头即可;也可以有参数; 例如:
function useCommonMethods(name) {
useEffect(() => {
console.log('${name}组件被创建');
return () => {
console.log('${name}组件被销毁');
}
}, [])
}
2.2、自定义hook练习
- 使用
User,Token的Context;
import React, {useContext} from 'react'
import {UserContext, TokenContext} from '../App'
import default function CustomHookContextDemo(){
const user = useContext(UserContext);
const token = useContext(TokenContext);
return <>
<h2>customHookDemo</h2>
</>
}
- 以上代码,在我们每次使用user和token时,都需要导入对应的Context,并且需要使用两次useContext;
- 我们可以抽取到一个自定义hook中;
function useUserToken() {
const user = useContext(UserContext);
const token = useContext(TokenContext);
return [user, token];
}
- 获取窗口滚动的位置
- 比如以下场景:获取创建滚动的位置
import React, { useEffect, useState } from 'react'
export default function CustomScrollPositionHook() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
}
document.addEventListener("scroll", handleScroll);
return () => {
document.removeEventListener("scroll", handleScroll);
}
}, [])
return (
<div style={{padding: "1000px 0", background: '#ddd'}}>
<h2 style={{position: 'fixed', top: '300px', left: '300px'}}>
CustomScrollPositionHook: {scrollPosition}
</h2>
</div>
)
}
- 但是如果每一个组件都有对应这样的一个逻辑,那么就很不好维护;
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.scrollY);
}
document.addEventListener("scroll", handleScroll);
return () => {
document.removeEventListener("scroll", handleScroll);
}
}, [])
return scrollPosition;
}