react-hook的使用总结

270 阅读8分钟

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绑定的那个组件;

先来回顾一下refforwardRef结合使用:

  • 通过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

useLayoutEffectuseEffect很相似,但是有区别;

  • 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, TokenContext;
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;
}