【问题标题】:React component subscribes too late. How to avoid losing the first event?React 组件订阅太晚。如何避免输掉第一场比赛?
【发布时间】:2017-03-08 01:42:43
【问题描述】:

我有一个按名称分组的事件流。

每个生成的 observable 都作为 events$ 属性传递给 Folder 组件,该组件在 componentDidMount() 中订阅它。

我遇到的问题是,Folder 的观察者错过了第一个事件。

要重现,请单击添加事件。您会看到Folder 呈现undefined,并且不会将其更新为发出事件中的数据。 Playground

const events$ = new Rx.Subject();

class Folder extends React.Component {
  constructor() {
    super();
    this.state = {};
  }

  componentDidMount() {
    this.props.events$.subscribe(e => {
      this.setState({
        name: e.name,
        permissions: e.permissions
      }); 
    });
  }

  componentWillUnmount() {
    this.props.events$.unsubscribe();
  }

  render() {
    const { name, permissions } = this.state;
    
    return (
      <div>
        {`[${permissions}] ${name}`}
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super();
    this.eventsByName$ = props.events$.groupBy(e => e.name);
    this.state = {};
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
     
      this.setState({
        [folder]: folderEvents$
      });
    });
  }
  
  onClick = () => {
    this.props.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  };
  
  render() {
    const folders = Object.keys(this.state);
    
    return (
      <div>
        <button onClick={this.onClick}>
          Add event
        </button>
        <div>
          {
            folders.map(folder => (
              <Folder events$={this.state[folder]} key={folder} />
            ))
          }
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <App events$={events$} />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>

为了获取丢失的事件,我使用了ReplaySubject(1):Playground

const events$ = new Rx.Subject();

class Folder extends React.Component {
  constructor() {
    super();
    this.state = {};
  }

  componentDidMount() {
    this.props.events$.subscribe(e => {
      this.setState({
        name: e.name,
        permissions: e.permissions
      });
    });
  }

  componentWillUnmount() {
    this.props.events$.unsubscribe();
  }

  render() {
    const { name, permissions } = this.state;
    
    return (
      <div>
        {`[${permissions}] ${name}`}
      </div>
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super();
    this.eventsByName$ = props.events$.groupBy(e => e.name);
    this.state = {};
  }
  
  componentDidMount() {
    this.eventsByName$.subscribe(folderEvents$ => {
      const folder = folderEvents$.key;
     
      const subject$ = new Rx.ReplaySubject(1);

      folderEvents$.subscribe(e => subject$.next(e));
      
      this.setState({
        [folder]: subject$
      });
    });
  }
  
  onClick = () => {
    this.props.events$.next({
      type: 'ADD_FOLDER',
      name: 'js',
      permissions: 400
    });
  };
  
  render() {
    const folders = Object.keys(this.state);
    
    return (
      <div>
        <button onClick={this.onClick}>
          Add event
        </button>
        <div>
          {
            folders.map(folder => (
              <Folder events$={this.state[folder]} key={folder} />
            ))
          }
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <App events$={events$} />,
  document.getElementById('app')
);
<head>
  <script src="https://unpkg.com/rxjs@5.2.0/bundles/Rx.js"></script>
  <script src="https://unpkg.com/react@15.4.2/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15.4.2/dist/react-dom.js"></script>
</head>
<body>
  <div id="app"></div>
</body>

现在,当点击 Add event 时,Folder 的观察者会看到该事件并正确呈现其数据。

这对我来说有点 hacky。

有没有更好的方法来组织代码以避免丢失事件的问题?

This question 可能会有所帮助。

【问题讨论】:

  • 我摆弄了您的代码,看起来第一次流没有真正正确设置,但groupBy 操作员吞下了错误。这是显示错误的“版本”:webpackbin.com/bins/-KeiIf-k2_Utqbn4Zw2u
  • 这似乎是 React 和 Rxjs 之间的时间问题。
  • @SebastianSebald 看看this question。

标签: reactjs rxjs rxjs5


【解决方案1】:

这确实是一个时间问题。普通主题不会缓存、缓冲或以其他方式保留通过它们发出的任何内容,所以发生的事情是这样的:

  1. 点击按钮
  2. onClick 调用events$.next(event) 并通知任何订阅者,目前只有eventsByName$,它正在侦听,因为它调用groupBy 并在componentDidMount 之前订阅
  3. eventsByName$ 订阅者调用 setState,这是批处理的,因为这里的调用堆栈是从 my answer to your previous question 中描述的 React 合成事件开始同步的
  4. React 一直等到 onClick 调用堆栈返回,然后刷新 setState 缓冲区
  5. React 重新渲染(请记住,在此之前已经通知了所有 event$ 订阅者,而 &lt;Folder&gt; 组件还不是其中之一,因为它甚至还没有被创建。李>
  6. 创建了一个新的&lt;Folder&gt; 组件,传入它的event$
  7. 新的&lt;Folder&gt; 挂载,导致它订阅event$ 它提供了,但错过了捕获您之前发出的ADD_FOLDER 事件的机会。

这就是使用ReplaySubject 有效的原因,它会重播您错过的最后一个事件。


总的来说,我建议远离这种事件模式。由于这是一个演示问题的人为示例,因此我无法确定您要尝试使用这些样式解决什么问题,但总的来说它与 Redux 有相似之处。如果您的应用程序需要这种模式,您可以考虑使用 Redux 或采用与 RxJS 相同的模式,使用 .scan()--which is here 的示例。很难说你是否需要这个,因为你给出的例子可以很容易地在没有 RxJS 的情况下完成,并且只需正常的 React onClick -> setState -> rerender -> 传递状态作为道具会更清楚(没有任何 Rx 在那里)

我强烈建议不要重新实现轮子——redux 有一个充满活力的中间件生态系统和优秀的开发工具,你会失去自己的。此外,您仍然可以将 RxJS 与 Redux 一起使用。我为此维护了一个库,redux-observable。

【讨论】:

  • 感谢 Jay 的详尽解答!
【解决方案2】:

我无法真正告诉您究竟是什么原因造成的,尽管这似乎是一个时间问题,因为 Subject 仅在第一个 click 事件之后才正确初始化。

但是,对于“更好”的模式:我建议使用与 Redux 类似的模式。意思是,创建一个连接到您的事件流的 HOC。已经有一个示例实现:https://github.com/MichalZalecki/connect-rxjs-to-react

您的实现与链接的实现之间的区别在于它使用context 而不是state。在这种情况下使用context 可能更安全,因为context 在 React 应用程序的生命周期内永远不会被销毁。

【讨论】:

    猜你喜欢
    • 2022-11-23
    • 2012-03-27
    • 1970-01-01
    • 1970-01-01
    • 2023-03-23
    • 2022-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多