React Hook

React Hook 的基础知识

React Hooks

React Hook 是用来实现逻辑的复用的加强版的函数组件,使用 hook 实现逻辑复用可以避免使用 render props 和 高阶组件多次嵌套形成的“嵌套地狱”。

优点

  1. React 高阶组件造成的嵌套地狱是指当使用多个高阶组件包装一个组件时,会产生很多层级的容器组件,导致代码可读性和维护性降低,例如,如果一个组件被这样包装:

    export default withSubscription(withMouse(withCat(WrappedComponent)));
    

    那么在 React Developer Tools 中,它的显示名称可能是这样的:

    <WithSubscription>
      <WithMouse>
        <WithCat>
          <WrappedComponent />
        </WithCat>
      </WithMouse>
    </WithSubscription>
    
  2. 类组件复用逻辑需要用到高阶函数,高阶函数是一个黑盒,外面的组件不知道内部的组件的逻辑,层层叠加之后,会给调试带来困难,而 hook 能够抽离逻辑到组件外部,很好的解决了这个问题;

  3. 类组件因为需要创建类组件的实例,所以对性能的开销较大,函数组件开销相对较小;

  4. 相较于也是复用逻辑的 HOC,每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。

  5. 函数组件不存在 state,但是 hook 弥补了这个缺点;

限制

  1. 不要在循环、条件判断或嵌套函数中使用(或者说初始化) Hooks,例如:

    if(exsit){
      const [name,setName] = useState("");
    }
    

    因为 Hooks 是基于闭包实现,在调用时每个 hook 都按顺序加入链表结构中,并赋予相对应的 index,在更新状态时,会根据 index 去修改 state,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。

  2. 只能在函数组件中使用 Hooks

useEffect

source 参数

useEffect 包含了 componentDidMount、componentDidUpdate、componentWillUnmount 三个生命周期,区别这三个生命周期主要是靠 source 参数:

useEffect(() => {
    console.log('componentDidMount || componentDidUpdate');

    return () => {
        console.log('componentWillUnmount');
    }
}, source);

当 source 没有值的时候,相当于 componentDidUpdate,每次更新都会被调用,且先调用 return 的函数,再调用外部的函数;

当 source 为空数组时,外部函数相当于 componentDidMount,return 函数相当于 componentWillUnmount ;

当 source 有值时,相当于 componentDidUpdate,但只会在 source 数组中的值变化时才会被调用,也是先调用 return 的函数,再调用外部的函数;

回调函数

useEffect 的回调函数,是对上一次调用 useEffect 进行清理,举个例子:

export default function HookTest() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('执行...', count);
    return () => {
      console.log('清理...', count);
    }
  }, [count]);
    
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1) }}>
        Click me
        </button>
    </div>
  );
}

// output:  
// 执行... 0
// 清理... 0 , 执行... 1
// 清理... 1 , 执行... 2

可以看到,在发生 state 变动的时候,会先触发 return 内的函数,且 return 内的函数获取到的是之前的 state。

那么再来看一下,useEffect 和渲染的执行顺序:

export default function HookTest() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('执行...', count);
    return () => {
      console.log('清理...', count);
    }
  }, [count]);
    
  return (
    <div>
      {console.log(`render ${count}`)}
      <p>You clicked {count} times</p>
      <button onClick={() => { setCount(count + 1) }}>
        Click me
        </button>
    </div>
  );
}

// output:  
// render 0 , 执行... 0
// render 1 , 清理... 0 , 执行... 1
// render 2 , 清理... 1 , 执行... 2

可以看到,render 要优先于 useEffect 触发。

useState

实现累加:将函数作为值传入,可以获取到上一次修改的 state,然后返回一个更新后的值:

setTranslateY(prevState => prevState + 1);

useLayoutEffect

useLayoutEffect 的作用和 useEffect 类似,写法上也类似:

useEffect(() => {
    // 执行副作用
    return () => { /* clean up */}
}, [dependency, arr])

useLayoutEffect(() => {
    // 执行副作用
    return () => { /* clean up */}
}, [dependency, array])

他们的不同点在于:

useEffect 是在渲染函数执行完成,并绘制到屏幕之后,再异步执行。大概流程如下:

  1. 触发渲染函数执行(改变状态,或者父组件重新渲染)
  2. React调用组件的渲染函数
  3. 屏幕中重绘完成
  4. 执行useEffect

useLayoutEffect 是在渲染函数执行之后,但是屏幕重绘前同步执行。大概流程如下:

  1. 触发渲染函数执行(改变状态,或者父组件重新渲染)
  2. React调用组件的渲染函数
  3. 执行 useLayoutEffect,并且 React 会等待它执行完成
  4. 屏幕中重绘完成

如何选择使用 useEffect 还是 useLayoutEffect?

useLayoutEffect 优先于 useEffect 触发,且在渲染完成之前触发,所以如果执行的操作会引起页面的闪烁,可以优先使用 useLayoutEffect,但是 useLayoutEffect 是同步执行的,会阻塞渲染,所以没有特殊需求的操作尽量使用 useEffect

useId

如果应用是 CSR(客户端渲染),id 是稳定的,不需要使用该 API,但如果应用是 SSR(服务端渲染),那么App.tsx 会经历:

React 在服务端渲染,生成随机id(假设为0.1234),这一步叫 dehydrate(脱水)

React 在客户端渲染,生成随机id(假设为0.6789),这一步叫 hydrate(注水)

最后客户端、服务端生成的 id 会出现不匹配的情况,于是 React 18 中新增了 useId 来生成不变的 ID

function Checkbox() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Do you like React?</label>
      <input id={id} type="checkbox" name="react"/>
    </>
  );
};

它的本质是通过组件在树中的层级计算出唯一ID,如下图所示

useTransition

当函数被 startTransition 包裹时,函数 setState 触发的渲染就会被标记为不紧急渲染,有可能被其他的紧急渲染所挤占,举个例子:

function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  
  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);  // 此处为不紧急渲染,如果有文本输入等紧急渲染任务,此渲染会被推迟
    })
  }

  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

useDeferredValue

其作用于 useTransition 类似,不过 useTransition 是包装更新任务成为延迟更新任务,而 useDeferredValue 是产生一个新的值,将这个值延迟。

useDeferredValue 仅延迟你传递给它的值。如果你想要在紧急更新期间防止子组件重新渲染,则还必须使用 React.memo 或 React.useMemo 记忆该子组件:

function Typeahead() {
  const query = useSearchQuery('');
  const deferredQuery = useDeferredValue(query);

  // Memoizing 告诉 React 仅当 deferredQuery 改变,
  // 而不是 query 改变的时候才重新渲染
  const suggestions = useMemo(() =>
    <SearchSuggestions query={deferredQuery} />,
    [deferredQuery]
  );

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">
        {suggestions}
      </Suspense>
    </>
  );
}

自定义 Hook 实现逻辑复用

自定义 hook 是 Hook 模式下实现 render props 或者高阶组件的方式,举个例子:

// useSetOnlineState.jsx
import { useState, useEffect } from 'react';

export const useIsOnline = (userInfo) => {
    const [isOnline,setIsOnline] = useState(null);
    
    useEffect(()=>{
        setIsOnline( useInfo.state === 'online' );
    });
    
    return isOnline;
}

上例新建了一个 useIsOnline 的 hook,根据传入的 userInfo 来判断并设置内部状态 isOnline 并将其返回,下面是该 hook 的调用方式:

function UserList(props) {
  const isOnline = useIsOnline( userItem.userInfo );

  return ( ... );
}

自定义 hook 除了可以直接返回值外,还可以返回设置默认值、内部状态的方法,这样实现的 hook 使用方法与原生 useState 相同:

// useRandomColor.jsx
import { useState, useEffect } from 'react';

export const useRandomColor = (initialColor) => {
    const [color,setColor] = useState(initialColor);
    
    const changeColor = (currentColor) =>{
        setColor(currentColor);
    }
    
    return [ color,changeColor ];
}

Hook 父组件调用子组件方法

首先在父组件中引入子组件,并父给子组件一个 ref 对象:

const childrenRef = useRef(null);

return <children ref={childrenRef} />

然后修改子组件,给子组件套上forwardRef,然后使用useImperativeHandle暴露 ref 自定义的实例值给父组件:

function children(props, ref) {
    useImperativeHandle(ref, () => ({
        hello: () => console.log('hello world!')
    }))
    return <h1>children</h1>
}
export default forwardRef(children);

最后在父组件中调用:

const something = () => childrenRef.current.hello();
作者

BiteByte

发布于

2020-08-08

更新于

2024-11-15

许可协议