React组件订阅太晚了。如何避免失去第一个事件?

时间:2017-03-08 01:42:43

标签: reactjs rxjs rxjs5

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

每个生成的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')
);
&#13;
<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>
&#13;
&#13;
&#13;

为了解决丢失的事件,我使用了ReplaySubject(1)Playground

&#13;
&#13;
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')
);
&#13;
<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>
&#13;
&#13;
&#13;

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

对我来说这感觉有些笨拙。

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

This question可能会有所帮助。

2 个答案:

答案 0 :(得分:2)

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

  1. 点击按钮
  2. onClick调用events$.next(event),它会通知任何正在侦听的订阅者{1}}正在侦听,因为它会调用eventsByName$并在之前订阅groupBy 1}}
  3. componentDidMount订阅者调用eventsByName$,这是批处理的,因为这里的调用堆栈是从my answer to your previous question
  4. 中描述的React合成事件开始同步的
  5. React等待,直到setState callstack返回,并刷新onClick缓冲区
  6. React rerenders(请记住,所有setState订阅者在此之前都已收到通知,而event$组件还不是其中之一,因为它还没有甚至被创造了。
  7. 创建了一个新的<Folder>组件,并传递了<Folder>
  8. 新的event$已安装,导致订阅<Folder>已提供,但错过了抓住之前发出的event$事件的机会。
  9. 这就是使用ADD_FOLDER工作的原因,它会重播您错过的最后一个事件。

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

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

答案 1 :(得分:1)

我无法确切地告诉你究竟是什么导致了这种情况,虽然这似乎是一个时间问题,因为Subject只能在第一个click事件后正确初始化。

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

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