React 组件订阅太晚了。如何避免输掉第一场比赛?
React component subscribes too late. How to avoid losing the first event?
我有一个按名称分组的事件流。
每个生成的可观察对象都作为 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>
现在,当单击 添加事件 时,Folder
的观察者会看到该事件并正确呈现其数据。
我觉得这有点老套。
有没有更好的组织代码的方式来避免事件丢失的问题?
可能会有帮助。
我真的不能告诉你到底是什么导致了这个,虽然它看起来像是一个时间问题,因为 Subject
只在第一个 click
事件之后正确初始化。
但是,对于 "better" 模式:我建议使用与 Redux 类似的模式。意思是,创建一个连接到您的事件流的 HOC。已经有一个示例实现:https://github.com/MichalZalecki/connect-rxjs-to-react
您的实现与链接的实现之间的区别在于它使用 context
而不是 state
。也许在这种情况下使用 context
更安全,因为 context
在 React 应用程序的生命周期中永远不会被销毁。
这确实是一个时间问题。普通 Subject 不会缓存、缓冲或以其他方式保留通过它们发出的任何内容,所以发生的事情是这样的:
- 点击按钮
onClick
调用 events$.next(event)
并通知任何订阅者,目前只有 eventsByName$
,它正在监听,因为它调用 groupBy
并在早些时候订阅componentDidMount
eventsByName$
订阅者调用 setState
,这是批处理的,因为通向此处的调用堆栈是同步从 React 合成事件开始的,如 中所述
- React 等到
onClick
调用堆栈 returns,然后刷新 setState
缓冲区
- React 重新呈现(请记住,所有
event$
订阅者在此之前都已收到通知,并且 <Folder>
组件 还不是其中之一 因为它甚至还没有创建。
- 创建了一个新的
<Folder>
组件,并传入其 event$
- 新的
<Folder>
安装,导致它订阅 event$
它已提供,但错过了捕获您之前发出的 ADD_FOLDER
事件的机会。
这就是使用 ReplaySubject
的原因,它会重播您错过的最后一个事件。
总的来说,我建议放弃这种事件模式。由于这是一个人为的例子来演示这个问题,我不能确定你试图使用这些风格来解决什么问题,但一般来说它与 Redux 有相似之处。如果您的应用程序需要这种模式,您可以考虑使用 Redux 或采用与 RxJS 相同的模式,使用 .scan()
——which is here 的示例。很难说你是否需要这个,因为你给出的例子可以很容易地在没有 RxJS 的情况下完成,并且只用普通的 React onClick -> setState -> rerender -> pass state as props(没有任何 Rx 在那里)会更清楚
虽然我强烈建议不要重新实现轮子——redux 有一个充满活力的中间件生态系统和优秀的开发工具,你会失去自己的。此外,您仍然可以将 RxJS 与 Redux 一起使用。我为此维护了一个库,redux-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>
现在,当单击 添加事件 时,Folder
的观察者会看到该事件并正确呈现其数据。
我觉得这有点老套。
有没有更好的组织代码的方式来避免事件丢失的问题?
我真的不能告诉你到底是什么导致了这个,虽然它看起来像是一个时间问题,因为 Subject
只在第一个 click
事件之后正确初始化。
但是,对于 "better" 模式:我建议使用与 Redux 类似的模式。意思是,创建一个连接到您的事件流的 HOC。已经有一个示例实现:https://github.com/MichalZalecki/connect-rxjs-to-react
您的实现与链接的实现之间的区别在于它使用 context
而不是 state
。也许在这种情况下使用 context
更安全,因为 context
在 React 应用程序的生命周期中永远不会被销毁。
这确实是一个时间问题。普通 Subject 不会缓存、缓冲或以其他方式保留通过它们发出的任何内容,所以发生的事情是这样的:
- 点击按钮
onClick
调用events$.next(event)
并通知任何订阅者,目前只有eventsByName$
,它正在监听,因为它调用groupBy
并在早些时候订阅componentDidMount
eventsByName$
订阅者调用setState
,这是批处理的,因为通向此处的调用堆栈是同步从 React 合成事件开始的,如 中所述
- React 等到
onClick
调用堆栈 returns,然后刷新setState
缓冲区 - React 重新呈现(请记住,所有
event$
订阅者在此之前都已收到通知,并且<Folder>
组件 还不是其中之一 因为它甚至还没有创建。 - 创建了一个新的
<Folder>
组件,并传入其event$
- 新的
<Folder>
安装,导致它订阅event$
它已提供,但错过了捕获您之前发出的ADD_FOLDER
事件的机会。
这就是使用 ReplaySubject
的原因,它会重播您错过的最后一个事件。
总的来说,我建议放弃这种事件模式。由于这是一个人为的例子来演示这个问题,我不能确定你试图使用这些风格来解决什么问题,但一般来说它与 Redux 有相似之处。如果您的应用程序需要这种模式,您可以考虑使用 Redux 或采用与 RxJS 相同的模式,使用 .scan()
——which is here 的示例。很难说你是否需要这个,因为你给出的例子可以很容易地在没有 RxJS 的情况下完成,并且只用普通的 React onClick -> setState -> rerender -> pass state as props(没有任何 Rx 在那里)会更清楚
虽然我强烈建议不要重新实现轮子——redux 有一个充满活力的中间件生态系统和优秀的开发工具,你会失去自己的。此外,您仍然可以将 RxJS 与 Redux 一起使用。我为此维护了一个库,redux-observable.