likes
comments
collection
share

灵活运用 React Hook

作者站长头像
站长
· 阅读数 16

前言

Hook 是 React 函数组件中十分重要的一个方面,如果仅仅停留在简单使用 useState、useEffect 是无法领略到 React Hook 的特别,所以快来感受一下吧~~~

组件生命周期

在学习 React Hook 之前,首先要对类组件和组件生命周期有了解。在 React 类组件中,组件生命周期阶段的划分并不唯一。在不同 React 版本中,生命周期函数也略有不同。本文所使用的 React 版本是16.14.0,后续所有操作都将基于此版本演示。

根据官方文档 React.Component – React,可以将组件的生命周期分为三个阶段和一种情况

  • 挂载/Mounting
  • 更新/Updation
  • 卸载/Unmounting
  • 错误处理/Error Handling

挂载/Mounting

挂载过程指组件实例创建并插入到 DOM 树的一系列过程,伴随四个生命周期函数的依次执行:

  1. constructor()
  2. static getDerivedStateFromProps()
  3. render()
  4. componentDidMount()

其中getDerivedStateFromProps是一个 static 函数,参数分别是props、state,返回新的 state。

更新/Updation

组件更新过程由 state 或 props 的变化触发,伴随五个生命周期函数的依次执行:

  1. static getDerivedStateFromProps()
  2. shouldComponentUpdate()
  3. render()
  4. getSnapshotBeforeUpdate()
  5. componentDidUpdate()

卸载/Unmounting

卸载过程指组件从 DOM 树中删除 DOM 的过程,只有一个函数在组件卸载后执行:

  1. componentWillUnmount()

错误处理/Error Handling

当 rendering 中、生命周期方法或是在子组件的 constructor 发生了错误,将调用以下方法:

  1. static getDerivedStateFromError()
  2. componentDidCatch()

对于类组件,开发者可以通过继承 React.PureComponentReact.Component 实现自己的组件。React.PureComponentReact.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数,所以在继承 React.PureComponent 无需实现该函数。

对于 React 函数组件,组件表示一个 JavaScript 函数,利用 JavaScript 的闭包机制,可以实现复杂的组件功能。相对于类组件,函数组件性能更好、更加简洁、易于理解、方便复用、可以使用方便的 React Hook。

Hook

React Hook 的本意是嵌入 React 函数组件状态生命周期的函数。与函数组件结合到一起,React Hook 的出现帮助我们解决了很多问题,它最重要的作用就是跨组件复用状态逻辑

使用 Hook 模拟生命周期

模拟生命周期是 React Hook 基本作用,让我们来从生命周期的角度来逐渐领略 React Hook 的风采。

模拟 constructor

很抱歉,React 函数组件无法模拟 constructor 函数,但是这并不意味着 React Hook 不能满足你的需求。如果需要初始化变量,你可以使用 useState 或 useRef 来满足需求。

同时模拟componentDidMountcomponentDidUpdate

Effect Hook,顾名思义,给函数组件提供执行 side effect 的功能。利用 Effect Hook,可以模拟 componentDidMountcomponentDidUpdatecomponentWillUnmount三个生命周期函数,当需要同时模拟componentDidMountcomponentDidUpdate时,使用以下方法:

function Example() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        // componentDidMount 和 componentDidUpdate 发生时执行
        console.log('mock compoentDidMount and componentDidUpdate');
    });
    
    return (
        <div>      
            <p>You clicked {count} times</p>      
            <button onClick={() => setCount(count + 1)}>        
                Click me      
            </button>    
        </div>
     );
}

现在当componentDidMountcomponentDidUpdate执行,都会执行输出语句。

模拟 componentDidMount

useEffect 函数的第二个参数可以传入数组依赖项,当依赖项为空时,useEffect 的函数参数只会在componentDidMount时执行。

function Example() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        // componentDidMount 发生时执行
        console.log('mock compoentDidMount');
    }, []);
    
    return (
        <div>      
            <p>You clicked {count} times</p>      
            <button onClick={() => setCount(count + 1)}>        
                Click me      
            </button>    
        </div>
     );
}

加入数组依赖项后,useEffect 的函数参数将在数组依赖项发生变化时执行。

模拟 componentDidUpdate

同样是利用 useEffect Hook,想要模拟componentDidUpdate函数,需要设置一个变量,帮助排除挂载的情况。

function Example() {
    const [count, setCount] = useState(0);
    const mounted = useRef(true);
    
    useEffect(() => {
        if (!mounted.current) {
          // componentDidUpdate 发生时执行 
          console.log("mock componentDidUpdate");
        }
        
        mounted.current = false;
    });
    
    return (
        <div>      
            <p>You clicked {count} times</p>      
            <button onClick={() => setCount(count + 1)}>        
                Click me      
            </button>    
        </div>
     );
}

模拟 componentWillUnMount

还是基于 useEffect Hook,在回调函数中返回的函数,可以实现componentWillUnMount的效果:

function Example() {
    const [count, setCount] = useState(0);
    
    useEffect(() => {
        // componentDidMount 和 componentDidUpdate 发生时执行
        console.log('mock compoentDidMount and componentDidUpdate');
        
        return function mockComponentWillUnMount() {
            // componentWillUnMount 发生时执行
            console.log('mock componentWillUnMount');
        };
    });
    
    return (
        <div>      
            <p>You clicked {count} times</p>      
            <button onClick={() => setCount(count + 1)}>        
                Click me      
            </button>    
        </div>
     );
}

模拟 shouldCoponentUpdate

当组件 props 和 state 发生变化时,会执行 shouldComponentUpdate函数询问是否进行 render。官方提供了React.memo函数,来帮助我们实现shouldComponentUpdate方法。

const newComponent = React.memo((props) => {
    // your component
});

React.memo将原来的组件包装成了一个新的组件,新组件会对 props 的新旧值进行浅比较,如果认为不同,就进行 render。当然也可以传入第二个参数,自定义比较函数。

const newComponent = React.memo(
    (props) => {
        // your component
    }, 
    (prevProps, nextProps) => {
        // return true or false
    }
)

上述只能对 props 引起的组件更新进行控制,对 state 引起的组件更新如何控制呢?众所周知,React 函数组件可以设置多个 state,而不像类组件那样所有状态都在一个 state 之内,函数组件相比类组件对 state 控制的粒度更细。

假如我们有下面一个需求,将点击次数随机展示,其类组件实现如下:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      value: true,
      countOfClicks: 0
    };
    this.pickRandom = this.pickRandom.bind(this);
  }

  pickRandom() {
    this.setState({
      value: Math.random() > 0.5,
      countOfClicks: this.state.countOfClicks + 1,
    });
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.state.value != nextState.value;
  }

  render() {
    return (
      <div>
        <p>
            <b>{this.state.value.toString()}</b>
        </p>
        <p>
            Count of clicks: 
            <b>{this.state.countOfClicks}</b>
        </p>
        <button onClick={this.pickRandom}>
          Click to randomly select: true or false
        </button>
      </div>
    );
  }
}

这个需求就是在shouldComponentUpdate中对 state 进行了判断,当 value 值不同时,才允许更新组件,而 countOfClicks 发生改变时,并不更新组件。

如果在函数组件中,可以将 state 中的 value 和 countOfClicks 分开为两个 useState:

function Example {
  const [value, setValue] = useState(true);
  const [countOfClicks, setCountOfClicks] = useState(0);

  function pickRandom() {
    setValue(Math.random() > 0.5);
    setCountOfClicks(countOfClicks + 1);
  }

  return (
    <div>
      <p>
        <b>value: {value ? "true" : "false"}</b>
      </p>
      <p>
        Count of clicks: <b>{countOfClicks}</b>
      </p>
      <button onClick={pickRandom}>
        Click to randomly select: true or false
      </button>
    </div>
  );
}

这时候每点击一次按钮,value 和 countOfClicks 都会更新,都会 render 组件,这与类组件行为并不一致,那么怎么避免这种情况?我们可以使用 useRef 而不是 useState 来控制 countOfClicks 。

function Example() {
  const [value, setValue] = useState(true);
  const countOfClicksRef = useRef(0);

  function pickRandom() {
    setValue(Math.random() > 0.5);
    countOfClicksRef.current = countOfClicksRef.current + 1;
  }

  return (
    <div>
      <p>
        <b>value: {value ? "true" : "false"}</b>
      </p>
      <p>
        Count of clicks: <b>{countOfClicksRef.current}</b>
      </p>
      <button onClick={pickRandom}>
        Click to randomly select: true or false
      </button>
    </div>
  );
}

得益于函数组件对 state 细粒度的控制,我们可以使用 useRef 和 useState 实现当只有 value 变化时才会更新组件。

模拟 static getDerivedStateFromProps

一般来说,模拟getDerivedStateFromProps并不是常见的情况,可以利用 useState 声明一个 state,然后在函数组件执行过程中更新 state。

function ScrollView({row}) {
    const [isScrollingDown, setIsScrollingDown] = useState(false);
    const [prevRow, setPrevRow] = useState(null);
    
    if (row !== prevRow) {
        // Row changed since last render. Update isScrollingDown.
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
    }
    
    return `Scrolling down: ${isScrollingDown}`;
}

当 props 发生变化时,进入 if 语句后,更新 state,组件返回时将会使用更新后的 state,既保证了getDerivedStateFromProps的效果,也保证了声明周期执行的顺序。

有一个值得注意的点就是不要省略 if 语句,这个if 语句判断了 props 和原来的值是否一致,保证了 props 是真的发生了变化。因为 setState 同样会导致组件 re-render,组件 render 同样会执行 if 语句,如果没有 if 语句进行判断的话,直接执行 if 内部语句,那就不是模拟getDerivedStateFromProps而是getDerivedStateFrom**State**了。

模拟render

render函数就不在这里做过多介绍,render函数可以返回 JSX 语法包装的 React 元素(也可以返回其他类型的值),React 会将返回的元素 commit 到 DOM 树中,实现 UI 变化。

函数组件与类组件不同之处在于,函数组件直接返回 JSX,而 render 函数还有函数体,可以在返回 JSX 之前,执行一些语句,例如打印一些日志。在函数组件中同样可以满足需求,将打印日志语句在紧挨着 return 语句上方即可。

function Example() {
    console.log('log');
    return <div>world</div>
}

模拟 getSnapshotBeforeUpdate

在更新阶段,render函数执行完,将会执行getSnapshotBeforeUpdategetSnapshotBeforeUpdate函数在 React 元素 commit 到 DOM 树之前执行,函数可以获得新旧的 props 和 state 值,你可以从中获取一些信息,并将信息返回。返回的信息将在 componentDidUpdate生命周期函数中获得。

下面是一个聊天列表的例子,功能是有新消息时,自动将列表滑动到最后一条消息。其类组件的实现为:

getSnapshotBeforeUpdate (prevProps, prevState) {
    if (this.state.chatList > prevState.chatList) {
      const chatThreadRef = this.chatThreadRef.current;
      return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
    }
    return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const chatThreadRef = this.chatThreadRef.current;
      chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
    }
}

用 Hook 实现此过程需要实现 getSnapshowBeforeUpdatecomponentDidUpdate两个方法,可以通过自定义 Hook 的方法实现需求,将此 Hook 函数命名为useGetSnaptshotBeforeUpdate

  1. 首先需要实现获得 prevProps 和 prevState 的自定义 Hook:
const usePrevPropsAndState = (props, state) => {
  const prevPropsAndStateRef = useRef({ props: null, state: null })
  const prevProps = prevPropsAndStateRef.current.props
  const prevState = prevPropsAndStateRef.current.state

  useEffect(() => {
    prevPropsAndStateRef.current = { props, state }
  })

  return { prevProps, prevState }
}
  1. 实现useGetSnapshowBeforeUpdate

通过 mounted 来判别是否是挂载阶段,因为getSnapshotBeforeUpdate是不在挂载阶段执行的,只在更新阶段执行。然后,更新内部变量 snapshot。最后,制定自定义的 Hook useComponentDidUpdate,传出供组件使用。

const useGetSnapshotBeforeUpdate = (cb, props, state) => {
  const { prevProps, prevState } = usePrevPropsAndState(props, state);
  const snapshot = useRef(null);
  const mounted = useRef(true);
  
  useLayoutEffect(() => {
    if (!mounted.current) {
        snapshot.current = cb(prevProps, prevState);
    }
    mounted.current = false;
  })

 const useComponentDidUpdate = cb => {
    useEffect(() => {
      if (!mounted.current) {
        cb(prevProps, prevState, snapshot.current);
      }
    });
  }
  
  return useComponentDidUpdate;
}

细心的同学已经发现了,为什么两段代码一个使用 useLayoutEffect,一个使用 useEffect,那是因为 useLayoutEffect 总是早于 useEffect执行,能够保证先更新 snaptshot 值,然后再执行useComponentDidUpdate函数。

最后来看看使用 React Hook 实现聊天组件的效果,通过useGetSnapshotBeforeUpdate实现聊天列表自动滚动到最新消息:

function Example(props) {
    const useComponentDidUpdate = useGetSnapshotBeforeUpdate(
        (_, prevState) => {
            if (state.chatList > prevState.chatList) {
                return chatThreadRef.current.scrollHeight - chatThreadRef.current.scrollTop;
            }
            return null;
        },
        props,
        state
    );

    useComponentDidUpdate((prevProps, prevState, snapshot) => {
        console.log({ snapshot });
        if (snapshot !== null) {
          chatThreadRef.current.scrollTop = chatThreadRef.current.scrollHeight - snapshot;
        }
    })
}

其他 Hook

除了上述出现的常用 useEffect、useState、useRef 等 Hook 之外,还有许多开箱即用的 Hook 供大家使用,这里就不一一介绍了,自行点击链接跳转官方文档查阅:

  • 基础 Hook

    • useState
    • useEffect
    • useContext
  • 其他 Hook

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue
    • useDeferredValue
    • useTransition
    • useId
  • 库 Hook

    • useSyncExternalStore
    • useInsertionEffect

自定义 Hook

在使用 Hook 模拟生命周期的过程中,经常使用自定义 Hook 满足需求。建立自定义 Hook,就是基于 React 已经提供的特性,以满足需求为前提,将组件中可以重用的逻辑代码抽离成可重用的函数。

在建立自定义 Hook 时,最好遵循 React 官方提供的一些规定:

  • 只能在 React 函数组件的顶级作用域使用 Hook
  • 最好使用 use 作为 Hook 函数名的开头
  • Hook 是具有执行顺序的,要注意 Hooks 执行顺序带来的影响
  • 不要用 Hook 来共享状态

下面是制作自定义 Hook 的一个例子,需求是展示好友是否在线,当组件挂载和更新时,订阅好友状态,当组件卸载时,取消订阅状态,下面是 FriendStatus 组件的代码:

function FriendStatus(props) {
    const [isOnline, setIsOnline] = useState ( null );

 useEffect ( () => {
 function  handleStatusChange ( status ) {
 setIsOnline (status. isOnline );
}

 ChatAPI . subscribeToFriendStatus (props. friend . id , handleStatusChange);
 return  () => {
 ChatAPI . unsubscribeFromFriendStatus (props. friend . id , handleStatusChange);
};
});

    if (isOnline === null) {
        return 'Loading...';
    }

    return isOnline ? 'Online' : 'Offline';
}

可以看出返回的 JSX 部分只和 isOnline 有关,剩余的其他代码比较孤立,我们可以将其他代码封装为一个自定义的Hook useFriendStatus:

function useFriendStatus(friendID) {  
    const [isOnline, setIsOnline] = useState(null);
    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }
        
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
    });
    
    return isOnline;
}

完成自定义 Hook 后,让我们在 FriendStatus 组件中使用 useFriendStatus Hook,我们的自定义的 Hook 就可以使用啦!

function FriendStatus(props) {  
    const isOnline = useFriendStatus(props.friend.id);
    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

值得注意的是,建立自定义 Hook 只是为了方便组件间复用logic,而不是复用 state,你可以把自定义 Hook 看为嵌入到 React 组件的一块代码,那么其 state 自然是隔离的而不是共用的。

结语

React Hook 一直是学习 React 的一个重点,其实熟悉 React Hook 并不困难,有过 React 类组件的开发经验的同学可以根据生命周期函数,很快接入到 React 函数组件的开发中,并熟练运用 React Hook。

参考

Getting Started – React

转载自:https://juejin.cn/post/7207724065056997413
评论
请登录