让我们忘掉组件的生命周期并开始尝试React effects吧!

184 阅读2分钟

翻译自 《Forget about component lifecycles and start thinking in effects》

React 组件一直依赖生命周期方法来解决副作用(side effect)。虽然生命周期方法可以解决,但代码通常过于冗长并且会产生很大的错误余地。

当组件卸载时很容易忘记“清理”副作用,或者在 props 更改时更新副作用。正如 Dan Abramov 所说:Don’t stop the data flow

React 最近引入了一种处理副作用的新方法:useEffect 钩子。 一开始可能会不适应将生命周期方法改为 useEffect 调用,因为我们压根儿不能快速将命令式生命周期方法转换为声明式 useEffect 调用!

监听websockets

以一个监听 websocket 的 ChatChannel 组件为例。

import React, { Component } from 'react';
import websockets from 'websockets';

class ChatChannel extends Component {
  state = {
    messages: [];
  }

  componentDidMount() {
    websockets.listen(
      `channels.${this.props.channelId}`,
      message => {
        this.setState(state => {
          return { messages: [...state.messages, message] };
        });
      }
    );
  }

  render() {
    // ...
  }
}

这样代码在浏览器中是可以正常运行工作的!但是,有两个隐藏的问题。

首先,我们从不清理副作用。当 ChatChannel 组件卸载时,websocket 侦听器仍处于注册状态。当一个新事件到来时,回调将运行并尝试更新一个不再存在的组件的状态。 所以我们需要实现一个 componentWillUnmount 方法来清理这个侦听器。

import React, { Component } from 'react';
import websockets from 'websockets';

class ChatChannel extends Component {
  state = {
    messages: [];
  }

  componentDidMount() {
    websockets.listen(
      `channels.${this.props.channelId}`,
      message => {
        this.setState(state => {
          return { messages: [...state.messages, message] };
        });
      }
    );
  }

  componentWillUnmount() {
    websockets.unlisten(`channels.${this.props.channelId}`);
  }

  render() {
    // ...
  }
}

channelId 属性发生变化时,仍会推送初始 channelId 的事件。

为了解决这个问题,我们添加了一个 componentDidUpdate 实现websocket始终处于监听最新频道的侦听器。

import React, { Component } from 'react';
import websockets from 'websockets';

class ChatChannel extends Component {
  state = {
    messages: [];
  }

  componentDidMount() {
    this.startListeningToChannel(this.props.channelId);
  }

  componentDidUpdate(prevProps) {
    if (this.props.channelId !== prevProps.channelId) {
      this.stopListeningToChannel(prevProps.channelId);
      this.startListeningToChannel(this.props.channelId);
    }
  }

  componentWillUnmount() {
    this.stopListeningToChannel(this.props.channelId);
  }

  startListeningToChannel(channelId) {
    websockets.listen(
      `channels.${channelId}`,
      message => {
        this.setState(state => {
          return { messages: [...state.messages, message] };
        });
      }
    );
  }

  stopListeningToChannel(channelId) {
    websockets.unlisten(`channels.${channelId}`);
  }

  render() {
    // ...
  }
}

这种用生命周期方法解决副作用的的问题在于它们是基于 React 渲染组件过程中的方式来实现。 在实践中,副作用真正要关心的是:数据会随着时间而改变,特别是props。

重构为useEffect

useEffect 重写我们的 ChatChannel 组件。

import React, { useEffect, useState } from 'react';
import websockets from 'websockets';

function ChatChannel({ channelId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    websockets.listen(
      `channels.${channelId}`,
      message => setMessages(messages => [...messages, message])
    );

    return () => websockets.unlisten(`channels.${channelId}`);
  }, [channelId]);

  // ...
}

我们没有牵扯到组件生命周期的细节,而是简单地基于 channelId 的当前值来声明一些listen副作用侦听器。我们还在闭包中返回一个unlisten的回调。 每当effect无效时,React 都会运行该回调并清理effect:比如当 channelId 更改时,或者当组件卸载时。

通过这种方式,开发者 不用考虑何时应该应用副作用,只需要声明副作用的依赖项,React 自己就知道何时需要运行、更新或清理。

这就是 useEffect 的强大之处。 websocket 监听器不关心组件的挂载和卸载,它关心的是 channelId 随时间变化的值。

问题不是“这个effect什么时候运行”,而是“这个effect与哪个state状态同步”

useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])

@ryanflorence on Twitter