带你彻底掌握React Hook的API特性

1,041 阅读13分钟

react-hook简介

介绍

  • Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
  • react-hook可以使用函数来声明一个有状态的组件。

react-hook能给我们带来什么?

  • 函数式编程。使用函数来生命组件,不用再使用class关键字来声明组件。使代码写法简洁,减少编译时大量冗余代码。
  • 更好的状态逻辑复用方案。在之前,HOC(高阶组件)和renderProps来解决状态逻辑复用困难问题
    • 层级嵌套过深,这两种模式都会通过组合来一层层的嵌套组件
    • 生命周期过多,逻辑代码逻辑阅读起来困难,出现bug调试困难
  • 自定义hook可以接受参数,并且返回任意类型的值。每一次调用hook,其中的状态和逻辑都是隔离的,这样可以随时将业务逻辑从ui中抽出来,并且可以做到多次复用,减少代码量,提升了开发效率。

react-hook基础api详解

useState

import React, { useState } from 'react';
const Example = ()=> {
  // 声明一个叫 “count” 的变量以及改变count状态的setCount函数
  const [count, setCount] = useState(0);
   return(
        <div>p
            <p>{count}</p>
            <button onClick={()=>setCount(count+1)}>+1</button>
        </div>
    )
 }

总结

  • useState()接收一个参数,作为当前声明state的初始值。注意:useState 只在组件初始化的时候执行,所以useState的初始值只在首次渲染时生效。所以可以在useState函数中执行首次渲染的操作,类似类组件中的constructorcomponentWillMount
    const Parent = () => {
        const [count, setCount] = useState(0);
        return(
            <div>p
                <p>{count}</p>
                <button onClick={()=>setCount(count+1)}>+1</button>
                // 接收一个count变量
                <Child count={count} />
            </div>
        )
    }
    const Child = ({count}) => {
        // 这里的useState初始值只在Child组件初始化时起作用,后面父组件的count如何变化都不会影响useState的初始值
        const [chldcount, setChldcount] = useState(count);
        return(
            <div>p
                <p>{count}</p>
                <button onClick={()=>setCount(count+1)}>+1</button>
            </div>
        )
    }
    
    
  • useState返回一个数组,值为当前的state以及更新state函数,一般为了识别方便,利用数组解构将值声明为xxxsetxxx两个值。
  • 使用多个更新state函数是会触发多次render,并不会像类组件中的setState合并state触发一次render。所以有相关联的state建议使用一个useState
const Demo = () => {
  // 声明多个 state 变量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
  
  changeState = () =>{
    // 这里会触发三次render,如果组件render一次消耗的性能过大,则不建议这样去做状态更新
    setAge(1);
    setFruit('apple');
    setTodos([{text:'学习怎么折腾'])
  }
  • 可以自己写一个自定义hook去解决多次更新state导致多次render的情况。建议需要修改多个并且逻辑复杂的state时,使用useReducer。
// 自定义hook,合并state修改
const useLegacyState = (states = {}) => {
    const [value, setValue] = useState(states);
    const setState = (param) => {
        if(typeof param === 'object'){
            const newStates = Object.assign({}, value, param);
            setValue(newStates)
        } else if(typeof param === 'function'){
            setValue(param)
        }else{
            throw new Error("参数错误");
        }
    }
    return [value, setState]
}

const Demo = () => {
  // 声明多个 state 变量
  const [state,setStete]= useLegacyState({age:42,fruit:banana,todos:'学习 Hook'})
  
  changeState = () =>{
    // 这样就只触发一次render
    setStete({
        age:1,
        fruit:'apple',
        todos:'学习怎么折腾'
    })
  }
  • 由于useState每次更新数据都是替换上一次的state,所以在使用useState时,如果在更新函数里传入同一个对象将无法触发更新。
  • useState的闭包问题
const demo = ()=>{
    const [count,setCount] = useState(0);
    const asyncChangeCount = ()=>{
        setTimeout(()=>{
            setCount(count+1)
        },3000)
    }
    return(
        <div>
            <a onclick={asyncChangeCount}>异步+1</a> // 首先点击异步+1,三秒后会改变count
            <a onclick={()=>setCount(count+1)}>同步+1</a> // 接着立马点击同步+1三次,这时count的值时3,3秒后count的值变为1
        </div>
    )
}

// 解决方法 修改asyncChangeCount方法
const asyncChangeCount = ()=>{
        setTimeout(()=>{
            // 这里修改
            setCount((count)=>count+1)
        },3000)
    }

关于 Hook 中的闭包:

useEffectuseMemouseCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state、props)。所以每一次这三种 Hook 的执行,反映的也都是当时的状态,无法获取最新的状态。对于这种情况,应该准确的使用依赖来对过时闭包做刷新。

useEffect

  • Effect 接收一个回调函数与一个数组类型的依赖,可以让你在函数组件中执行副作用操作,返回一个清除副作用的函数
useEffect(() => {
    document.title = `You clicked ${count} times`;
    
    window.addEventListener('load', loadHandle); // loadHandle 函数定义省略
    
    return () => {
      window.removeEventListener('load', loadHandle); // 执行清理:callback 下一次执行前调用
    };
  }, [count]); // 依赖数组,每次count发生变化时,useEffect都会重新执行一遍

总结

  • 执行时机: 在每次render之后执行一次,如果设置依赖为空数组则只在首次render之后执行操作,相当于类组件的componentDidMount,如果不传入依赖则相当于类组件的componentDidUpdate,返回的清除依赖的函数相当于类组件的componentWillUnmount

提示

与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect >不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect >不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

  • 依赖的比较是浅比较,传入对象、函数(引用类型参数)是无意义,很多死循环的bug都是依赖传入不正确导致的。从性能优化以及减少bug角度来看,建议依赖参数每次都要传入。
// countArray是个数组
const Demo = ({countArray}) =>{
    // 每次Demo组件更新,useEffect都会执行一遍
    useEffect(()=>{
        // do something
    },[countArray])
}

// 这里传countArray作为依赖或者不传依赖都会导致死循环
const Demo = () =>{
    const [countArray,setCountArray] = useState([]);
    // 每次Demo组件更新,useEffect都会执行一遍
    useEffect(()=>{
       setCountArray([])
    },[countArray])
}
  • 尽量使用在useEffect内部声明的函数。要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。
  const App = ({ someProp })=> {
    const doSomething =()=> {
      console.log(someProp);
    }

    useEffect(() => {
      doSomething();
    }, []); 
    // 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`),当someProp发生更新的时候,doSomething不能如实的打印出正确的值
  }

  const App = ({ someProp })=> {

    useEffect(() => {
     const doSomething =()=> {
      console.log(someProp);
    }
    doSomething();
    }, [someProp]); 
    // useEffect依赖了someProp,这样每次someProp发生更新时,doSomething都能打印正确的值。
  }
  • 可以在一个组件中使用多个useEffect来做逻辑分离。
const Demo = () => {
    useEffect(()=>{
        // 逻辑一
    })
    useEffect(()=>{
        // 逻辑二
    })
}
  • 每一次渲染后,useEffect都会获取渲染时的state和props。useRef保存任何可变化的值,.current属性总是取最新的值。就是相当于全局作用域,一处被修改,其他地方全更新,所以如果获取不到最新值时,可以使用useRef来将state和props保存起来使用。
function Counter() {
  const [count, setCount] = useState(0);

  // 当button按钮3在秒内点击3次
  // useEffect会在3秒后依次打印
  // You clicked 1 times
  // You clicked 2 times
  // You clicked 3 times

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

// 使用useRef
...
const [count, setCount] = useState(0);
const latestCount = useRef(count);

  // 当button按钮3在秒内点击3次
  // useEffect会在3秒后依次打印
  // You clicked 3 times
  // You clicked 3 times
  // You clicked 3 times

  useEffect(() => {
  latestCount.current = count;
    setTimeout(() => {
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
...

useReducer

  • useReducer是useState的替代方案,一旦组件内维护的useState过多(建议超过5个)或者state逻辑复杂时,就应该考虑使用useReducer,并且还能给触发深更新的组件做性能优化。(useState 是使用 useReducer 构建的,所以能用useState的地方一定能用useReducer)
  • useReducer接收一个(state,action)=>newState的reducer,并返回当前state以及其配套的dispatch函数。
// react官方demo
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

总结

  • useReducer返回的dispatch函数,React 会确保 它函数的标识是稳定的,并且不会在组件重新渲染时改变。就是说当子组件接受dispatch函数时,子组件不会因为dispatch是函数变量而重新渲染组件,如果将useState返回的改变state的函数传入到子组件时,子组件会重新渲染。
  • 同时使用两次dispatch函数,只会触发一次render。
  • 可以在一个组件里维护多个useReducer,useReducer也可以使用使用同一套reduce,可以提高代码复用性。

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。
//  创建一个React的上下文(context)
// defaultValue 如果父组件没有使用Provider,在子组件使用useContext时,defaultVadelue才会生效
const DemoContext = React.createContext(defaultVadelue);

function App() {
  return (
    <DemoContext.Provider value={{a:1}}>
      <Children /> // Children组件以及其子组件都可以使用{a:1}
    </DemoContext.Provider>
  );
}

const Children = () =>{
    const context = useContext(DemoContext); // context={a:1}
    ...
}

总结

  • useContext解决了父组件值透传到子组件中,的避免了多级 props 的层层透传问题
  • 注意:调用了 useContext 的组件总会在 context 值变化时重新渲染,如果context经常会发生变化,通过 React.memo() 可以很好的管理子组件的性能问题。
  • 使用useReducer搭配useContext构建小型redux
// Content.js
import React, { useReducer, createContext } from 'react'
const defaultState = {
    value: 0
}

function reducer(state, action) {
    switch(action.type) {
        case 'ADD_NUM':
            return { ...state, value: state.value + 1 };
        case 'REDUCE_NUM':
            return { ...state, value: state.value - 1 };
        default: 
            throw new Error();
    }
}
export const Context = createContext(null)

export function Content() {
    const [state, dispatch] = useReducer(reducer, defaultState)

    return (
        // 将dispatch和state传到Children的所有子组件中去
        <Context.Provider value={{state, dispatch}}>
        // 在子组件中就直接就可以用useContext()获取state和dispatch函数了
            <Children/>
        </Context.Provider>
    )
} 

useCallback

  • useCallback接收一个内联回调函数以及依赖项数组作为参数,并返回该回调函数的 memoized 版本。
    const App = () => {
        // 使用useCallback返回()=>console.log('222')的memoized版本,并只在组件首次渲染时生成,
        const fn = useCallback(()=>{
            console.log('222')
        },[])
        
        return(
            <div>
                <button onClick={fn}>点击</button>
            </div>
        )
    }

总结

  • useCallback返回一个memoizedCallback,在依赖未改变的时候,memoizedCallback总是指向同一个函数,也就是说useCallback返回了内联函数的缓存版本。
// 
let fn = null;
const App = () => {
    const [count, setCount] = useState(0);

    const consoleCount = useCallback(() => {
        console.log('6666',count); // 一直输出0
    }, [])
    
    // 首次渲染返回false,每当count发生变成重新渲染时返回true
    console.log('666', Object.is(fn, consoleCount)) 

    fn = consoleCount;
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={() => setCount(count + 1)}>点击+1</button>
            <button onClick={consoleCount}>输出count</button>
        </div>
    )
}
  • useCallback并不能防止函数的创建,即使useCallback的依赖数组为[],每次组件渲染的时候()=>console.log('222')函数还是会重新被创建。
  • useCallback应用最多的场景还是缓存了memoizedCallback,这样如果要将memoizedCallback传到子组件,可以在子组件使用React.memo来减少不必要的渲染。
let fun = null;
const Parent = ()=>{
    const [count,setCount] = useState(0);
    const memoSetCount = useCallback((count)=>setCount(count),[])
    
    // 首次渲染返回false,count改变重新渲染返回true
    console.log('77',Object.is(fun,memoSetCount))
    fun=memoSetCount;
    return(
        <div>
            <h1>{count}</h1>
            <Child setCount={memoSetCount} />
        </div>
    )
}

// count改变后Child组件不会重新渲染
const Child = React.memo(({setCount})=>{
    return(
        <button onClick={() => setCount((count)=>count+1}>点击+1</button>
    )
})
  • React.memo 和 React.useCallback一定记得需要配对使用,缺了一个都可能导致性能不升反“降”,毕竟无意义的浅比较也是要消耗那么一点点的性能。

useMemo

  • 和useCallback不同的是,useMemo返回一个"缓存值",把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值
const Parent = () => {
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    const handleSetCount = () => {
        setCount(count + 1);
    }

    const handleCalNumber = () => {
        setNumber(number + 1);
    }


    return (
        <div>
            // 改变count时,Child组件会重新渲染,
            <button onClick={handleSetCount}>count is : {count} </button>
            // 改变number时,Child组件会重新渲染,
            <button onClick={handleCalNumber}>numberis : {number} </button>
            <hr />
            <Child count={count} />
        </div>
    );
}

// Child缓存了每次count值改变后的组件值,如果count不发生变化,Child只会返回缓存值,不会重新渲染useMemo中的组件
const Child = ({ count }) => {
    return useMemo(() => {
        console.log('111');
        return (
            <div>
                <h1>{count}</h1>
            </div>
        )
    }, [count])
}

总结

  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
  • useCallback和uesMemo并没有阻止内联函数的执行,反而会加上一些缓存和对比依赖的额外操作(性能成本),如果使用useMemo让代码变得难以阅读理解(理解成本)或者优化的效果并没有那么明显,则不建议大量使用。
  • useCallback和uesMemo最大的作用就是能够保持在依赖不变的情况下,返回的值和函数的引用不变。对于一些将函数或者值作为useEffect的依赖项以及组件的memo优化方便还是很有作用,但还是要评估下优化的作用是不是很大。(渲染次数,渲染次数,计算量)

useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
    // 常见的用法是用作获取DOM的实例
    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>
        </>
      );
    }
  • useRef() 比 ref 属性更有用。其类似于在 class 中的this,可以方便地保存任何可变值。

总结

  • createRef和useRef有什么区别?createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
const App = () => {
    const [count, setCount] = useState(0)
    // createRef会在每次渲染时重新初始化ref对象
    const creRef = createRef();
    // useRef 会在每次渲染时返回同一个 ref 对象。
    const ref = useRef();

    useEffect(() => {
        creRef.current = count;
        ref.current = count;
    })
    console.log('creRef',creRef.current) // 返回 null null null null
    console.log('ref',ref.current) // 返回 0,1,2,3

    return (
        <div>
            <h1>{count}</h1>
            <button onClick={setCount.bind(null, count + 1)}>点击+1</button>
        </div>
    )
}
  • 使用useRef能够解决大部分hook所遇到的问题,由于useEffect,useCallback,useMemo都会产生闭包,如果没有正确的指定依赖或者没有合理使用闭包都会拿不到想要获取的值。
  • 由于每次useRef()的.current属性总是取最新的值。就是相当于全局作用域,一处被修改,其他地方全更新。所以我们总能够用.current获取最新的值。
  • 在使用hook开发时,如果想拿到上一次更新的值,也可以用useRef来实现
    const usePreValue = value = >{
        const valueRef = useRef();
        // 获取上一次的值
        const preValue = valueRef.current;
        // 更新current
        valueRef.current = value;
        // 返回上一次的值
        return preValue;
    }
    // 使用
    const App = () => {
        const [count, setCount] = useState(0)
        const preValue = usePreValue(count);
    
        return (
            <div >
                <h1>current:{count}</h1>
                <h1>preValue:{preValue}</h1>
                <button onClick={setCount.bind(null, count + 1)}>点击+1</button>
            </div>
        )
    }

useImperativeHanle

  • 定义: useImperativeHandle(ref, createHandle, [deps])
    • ref 接收父组件传递的ref实例,并且将子组件的ref实例属性转发到父组件的ref上
    • 暴露子组件中ref实例中的那些属性
    • 依赖项,当监听的依赖发生变化,useImperativeHandle 才会重新将子组件的实例属性输出到父组件
  • 作用:可以把子组件的ref实例的属性值自定义的暴露给父组件的ref实例
    const FancyInput(props, ref) {
      const inputRef = useRef();
      useImperativeHandle(ref, () => ({
        // 将inputRef.current.focus转发到父组件的ref中
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return <input ref={inputRef} />;
    }
    
   const  FancyRefInput = forwardRef(FancyInput);
   const Parent = ()=>{
        const parentRef = useRef();
        return(
            <div>
                <FancyRefInput ref={parentRef}/>
                <button onClick={()=>parentRef.current.focus()}>
                    父组件调用子组件的focus
                </button>
            </div>
        )
    }

总结

  • useImperativeHandle 可以自定义子组件暴露给父组件的ref实例属性,所以说你也可以自定义focus方法,在focus方法中做更多事情。
  • useImperativeHandle 应当与 forwardRef 一起使用。
  • 如果不想向父组件暴露ref的所有属性就可以用到这个hook,但是实际工作中时很少用到这个hook。

useLayoutEffect

const App = () => {
    const [count, setCount] = useState(0);
    const [number, setNumber] = useState(0);

    useEffect(() => {
        if (count === 0) {    
            setCount(10 + Math.random() * 200);
        }
    }, [count]);
    
    useLayoutEffect(() => {
        if (number === 0) {    
            setNumber(10 + Math.random() * 200);
        }
    }, [number]);
    
    
    // 点击DIV重置count
    return (
        <div>
        	//  点击count时,页面有短暂的闪烁
            <h1 onClick={() => setCount(0)}>{count}</h1>
            // 点击setNumber时,页面不会闪烁,但是会比点击count有短暂的延迟
            <h1 onClick={() => setNumber(0)}>{number}</h1>
        </div>
    );
};

总结

  • useLayoutEffect要比useEffect更早的触发执行;
  • 除非要修改DOM并且不让用户看到修改DOM的过程,才考虑使用useLayoutEffect,否则应当使用useEffect。
  • useLayoutEffect和useEffect的使用方法都是一样的,接收一个函数和依赖项,返回一个回调函数清除副作用。
  • useLayoutEffect和平常写的ClassComponent的'componentDidMount'和'componentDidUpdate'同时执行。

useDebugValue

  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签,主要用作自定义hook的封装。
// 自定义hook
const useHook = () => {
    const [count, setCount] = useState(0);
    // 再react-devtool中显示自定义标签
    useDebugValue(count > 5 ? 'count>5' : 'count<=5');

    const mySetCount = () => {
        setCount(count + 2);
    }

    return [count, mySetCount];
}

const App = () => {
    const [count, setCount] = useHook();

    return (
        <div>
            <h1>{count}</h1>
            <button onClick={setCount}>count+2</button>
        </div>
    )
}

总结

  • react官方并不推荐项每一个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。
  • useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。useDebugValue(date, date => date.toDateString());

react-hook的渲染逻辑

  • 函数组件更新时,组件外层的逻辑(比如定义的变量,计算)都会重新自执行一遍。而class组件中外层逻辑都是在new的时候才会重新执行一遍。不用担心Hook 会因为在渲染时创建函数而变慢,在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
  • 每次使用useState中的更新状态函数,都会导致组件re-render一次,如果在一个操作内使用了两个更新状态函数,则会re-render两次。
  • 使用useReducer时,在同一个操作里面使用两个dispatch函数更变状态,只会让组件重新渲染一次。
  • useState的更新函数更新相同的值时,组件不会重新渲染。
  • 父组件更新时,子组件都会重新渲染,可以用react.memo来对props进行浅比较,阻止子组件更新。

总结

  • 将组件拆分的更加细致,使用hook的效果会越好。
  • 状态更新逻辑复杂时,建议使用useReducer。