使用 react-router 从 this.props.children 访问 React 组件
Access React component from this.props.children with react-router
我的网站具有以下 react-router
和 react-router-relay
设置:
main.jsx:
<Router history={browserHistory} render={applyRouterMiddleware(useRelay)}>
<Route path="/:classroom_id" component={mainView} queries={ClassroomQueries}>
<IndexRoute component={Start} queries={ClassroomQueries} />
<Route path="tasks" component={TaskRoot} queries={ClassroomQueries}>
<IndexRoute component={TaskList} queries={ClassroomQueries} />
<Route path="task/:task_id" component={TaskView} queries={ClassroomTaskQueries} />
<Route path="task/:task_id/edit" component={TaskEdit} queries={ClassroomTaskQueries} />
</Route>
</Route>
</Router>
该站点是一个虚拟教室,教师可以在其中为学生创建任务。这些任务也可以编辑。
当用户在 TaskEdit
中编辑任务并点击保存时,将应用更改并将用户重定向到 TaskList
。现在,我想在编辑任务后向用户发送“您的更改已保存!”消息。
我为此创建了一个 bootstrap alert 组件 (<Alert>
)。
为了便于说明,假设我有以下 mainView
组件(检查上面的路由器):
mainView.js:
render() {
<div>
<Menu />
<Row>
<Col md={3}>
<Sidebar />
</Col>
<Col md={9}>
{this.props.children} <-- Start, TaskRoot, etc go here
</Col>
</Row>
<Alert someProps={...} /> <-- Popup component
</div>
}
通常,我会在活动组件之外创建此警报组件,然后将某种显示警报的函数作为道具传递给活动组件。类似于显示的内容 here.
但是,这仅适用于特定组件。但是因为 <Alert>
位于我页面的根级别(连同菜单栏、边栏和其他在所有页面上都是静态的东西),由于 {this.props.children}
,我无法将其作为道具传递。
请允许我再次说明我想要实现的流程:
- 用户在
<TaskEdit>
中保存了对任务的编辑。
- 编辑已保存,用户返回
<TaskList>
。
- 紧接着,我希望
<Alert>
触发,并显示一条消息。
我看到其他解决方案建议您可以克隆 React 元素的所有子元素并应用打开 alert
所需的道具,但由于 react-router
的性质,这不会'好像真的没用。
我怎样才能完成上述任务?有没有办法让组件可以全局访问?请注意,我不想在每个组件中重新创建 <Alert>
。整个DOM.
只有一个这样的组件
简单回答:
将您的警报组件放在顶层视图中一次,然后使用一些 pubsub 或事件总线库来更新它。
在您的 Alert 组件 onComponentDidMount 函数中,您侦听特定事件和数据并相应地更新其状态(可见性和消息)
在您的任务保存功能中,您将事件发布到事件总线。
更高级的回答:
使用 React 助焊剂库 https://facebook.github.io/flux/docs/overview.html
一个简单的解决方案是使用 context
feature:
const MainView = React.createClass({
childContextTypes: {
triggerAlert: PropTypes.func.isRequired,
},
getChildContext() {
return {
triggerAlert: alert => this.setState({ alert }),
};
},
getInitialState() {
return {
alert: null,
};
},
render() {
return (
<div>
{this.props.children}
<Alert alert={this.state.alert} />
</div>
};
},
});
const AnyChild = React.createClass({
contextTypes: {
triggerAlert: PropTypes.func.isRequired,
},
handleClick() {
this.context.triggerAlert('Warning: you clicked!');
},
render() {
return <button onClick={this.handleClick} />
},
});
但是,这与命令式相关API。这也意味着所有生成警报的 children 组件都必须 re-implement 一些样板文件。
您还可以将与警报呈现相关的所有内容移动到它们自己的组件中,将您的实现隐藏在更简单的 API:
const AlertRenderer = React.createClass({
childContextTypes: {
addAlert: PropTypes.func.isRequired,
},
getChildContext() {
return {
addAlert: this.addAlert,
};
},
getInitialState() {
return {
alerts: [],
};
},
addAlert(alert) {
this.setState(({ alerts }) =>
({ alerts: alerts.concat([alert]) })
);
const remove = () => {
this.setState(({ alerts }) =>
({ alerts: alerts.splice(alerts.indexOf(alert), 1) })
);
};
const update = () => {
this.setState(({ alerts }) =>
({ alerts: alerts.splice(alerts.indexOf(alert), 1, alert) })
);
};
return { remove, update };
},
render() {
return (
<div>
{this.props.children}
{this.state.alerts.map(alert =>
<this.props.component
key={alert} // Or whatever uniquely describes an alert
alert={alert}
/>
)}
</div>
);
},
});
const Alert = React.createClass({
contextTypes: {
addAlert: PropTypes.func.isRequired,
},
propTypes: {
alert: PropTypes.any.isRequired,
},
componentWillMount() {
const { update, remove } = this.context.addAlert(this.props.alert);
this.updateAlert = update;
this.removeAlert = remove;
},
componentWillUnmount() {
this.removeAlert();
},
componentWillReceiveProps(nextProps) {
this.updateAlert(nextProps.alert);
},
render() {
return null;
},
});
您的应用程序代码将变为:
const AnyChild = React.createClass({
getInitialState() {
return {
alert: null,
};
},
handleClick() {
this.setState({
alert: 'Warning: you clicked the wrong button!',
});
},
render() {
return (
<div>
<button onClick={this.handleClick}>
</button>
{this.state.alert &&
<Alert alert={this.state.alert} />
}
</div>
);
},
});
const MainView = React.createClass({
render() {
return (
<AlertRenderer component={MyAlertComponent}>
{this.props.children}
</AlertRenderer>
);
},
});
这种特殊方法的一个问题是,每当警报更新时,AlertRenderer 将 re-render all children。这会导致无限递归,因为 AlertSub 的 componentWillReceiveProps 生命周期再次触发。请注意,仅当您的组件具有可变道具并且未实现 shouldComponentUpdate
.
时才会出现此问题
解决此问题的一个简单方法是创建一个条件 children 渲染器,它仅在检测到其 children 已更改时更新。
下面的示例代码为 单个 child 创建了这样一个条件渲染器。为了有条件地呈现多个 children,您需要将其包装到一个额外的 DOM 组件中。
const ShouldSingleChildUpdate = React.createClass({
shouldComponentUpdate(nextProps) {
return nextProps.children !== this.props.children;
},
render() {
return this.props.children;
},
});
const AlertRenderer = React.createClass({
/* ... */
render() {
return (
<div>
<ShouldSingleChildUpdate>
{this.props.children}
</ShouldSingleChildUpdate>
{this.state.alerts.map(alert =>
<this.props.component
key={alert} // Or whatever uniquely describes an alert
alert={alert}
/>
)}
</div>
);
},
});
请注意,这种方法适用于任何类型的模态框,其中可以随时在屏幕上显示多个模态框。在您的情况下,由于您在任何时候都应该只在屏幕上显示一个 Alert
组件,因此您可以通过仅在状态中存储一个警报来简化 AlertRenderer
代码。
我的网站具有以下 react-router
和 react-router-relay
设置:
main.jsx:
<Router history={browserHistory} render={applyRouterMiddleware(useRelay)}>
<Route path="/:classroom_id" component={mainView} queries={ClassroomQueries}>
<IndexRoute component={Start} queries={ClassroomQueries} />
<Route path="tasks" component={TaskRoot} queries={ClassroomQueries}>
<IndexRoute component={TaskList} queries={ClassroomQueries} />
<Route path="task/:task_id" component={TaskView} queries={ClassroomTaskQueries} />
<Route path="task/:task_id/edit" component={TaskEdit} queries={ClassroomTaskQueries} />
</Route>
</Route>
</Router>
该站点是一个虚拟教室,教师可以在其中为学生创建任务。这些任务也可以编辑。
当用户在 TaskEdit
中编辑任务并点击保存时,将应用更改并将用户重定向到 TaskList
。现在,我想在编辑任务后向用户发送“您的更改已保存!”消息。
我为此创建了一个 bootstrap alert 组件 (<Alert>
)。
为了便于说明,假设我有以下 mainView
组件(检查上面的路由器):
mainView.js:
render() {
<div>
<Menu />
<Row>
<Col md={3}>
<Sidebar />
</Col>
<Col md={9}>
{this.props.children} <-- Start, TaskRoot, etc go here
</Col>
</Row>
<Alert someProps={...} /> <-- Popup component
</div>
}
通常,我会在活动组件之外创建此警报组件,然后将某种显示警报的函数作为道具传递给活动组件。类似于显示的内容 here.
但是,这仅适用于特定组件。但是因为 <Alert>
位于我页面的根级别(连同菜单栏、边栏和其他在所有页面上都是静态的东西),由于 {this.props.children}
,我无法将其作为道具传递。
请允许我再次说明我想要实现的流程:
- 用户在
<TaskEdit>
中保存了对任务的编辑。 - 编辑已保存,用户返回
<TaskList>
。 - 紧接着,我希望
<Alert>
触发,并显示一条消息。
我看到其他解决方案建议您可以克隆 React 元素的所有子元素并应用打开 alert
所需的道具,但由于 react-router
的性质,这不会'好像真的没用。
我怎样才能完成上述任务?有没有办法让组件可以全局访问?请注意,我不想在每个组件中重新创建 <Alert>
。整个DOM.
简单回答:
将您的警报组件放在顶层视图中一次,然后使用一些 pubsub 或事件总线库来更新它。
在您的 Alert 组件 onComponentDidMount 函数中,您侦听特定事件和数据并相应地更新其状态(可见性和消息)
在您的任务保存功能中,您将事件发布到事件总线。
更高级的回答:
使用 React 助焊剂库 https://facebook.github.io/flux/docs/overview.html
一个简单的解决方案是使用 context
feature:
const MainView = React.createClass({
childContextTypes: {
triggerAlert: PropTypes.func.isRequired,
},
getChildContext() {
return {
triggerAlert: alert => this.setState({ alert }),
};
},
getInitialState() {
return {
alert: null,
};
},
render() {
return (
<div>
{this.props.children}
<Alert alert={this.state.alert} />
</div>
};
},
});
const AnyChild = React.createClass({
contextTypes: {
triggerAlert: PropTypes.func.isRequired,
},
handleClick() {
this.context.triggerAlert('Warning: you clicked!');
},
render() {
return <button onClick={this.handleClick} />
},
});
但是,这与命令式相关API。这也意味着所有生成警报的 children 组件都必须 re-implement 一些样板文件。
您还可以将与警报呈现相关的所有内容移动到它们自己的组件中,将您的实现隐藏在更简单的 API:
const AlertRenderer = React.createClass({
childContextTypes: {
addAlert: PropTypes.func.isRequired,
},
getChildContext() {
return {
addAlert: this.addAlert,
};
},
getInitialState() {
return {
alerts: [],
};
},
addAlert(alert) {
this.setState(({ alerts }) =>
({ alerts: alerts.concat([alert]) })
);
const remove = () => {
this.setState(({ alerts }) =>
({ alerts: alerts.splice(alerts.indexOf(alert), 1) })
);
};
const update = () => {
this.setState(({ alerts }) =>
({ alerts: alerts.splice(alerts.indexOf(alert), 1, alert) })
);
};
return { remove, update };
},
render() {
return (
<div>
{this.props.children}
{this.state.alerts.map(alert =>
<this.props.component
key={alert} // Or whatever uniquely describes an alert
alert={alert}
/>
)}
</div>
);
},
});
const Alert = React.createClass({
contextTypes: {
addAlert: PropTypes.func.isRequired,
},
propTypes: {
alert: PropTypes.any.isRequired,
},
componentWillMount() {
const { update, remove } = this.context.addAlert(this.props.alert);
this.updateAlert = update;
this.removeAlert = remove;
},
componentWillUnmount() {
this.removeAlert();
},
componentWillReceiveProps(nextProps) {
this.updateAlert(nextProps.alert);
},
render() {
return null;
},
});
您的应用程序代码将变为:
const AnyChild = React.createClass({
getInitialState() {
return {
alert: null,
};
},
handleClick() {
this.setState({
alert: 'Warning: you clicked the wrong button!',
});
},
render() {
return (
<div>
<button onClick={this.handleClick}>
</button>
{this.state.alert &&
<Alert alert={this.state.alert} />
}
</div>
);
},
});
const MainView = React.createClass({
render() {
return (
<AlertRenderer component={MyAlertComponent}>
{this.props.children}
</AlertRenderer>
);
},
});
这种特殊方法的一个问题是,每当警报更新时,AlertRenderer 将 re-render all children。这会导致无限递归,因为 AlertSub 的 componentWillReceiveProps 生命周期再次触发。请注意,仅当您的组件具有可变道具并且未实现 shouldComponentUpdate
.
解决此问题的一个简单方法是创建一个条件 children 渲染器,它仅在检测到其 children 已更改时更新。
下面的示例代码为 单个 child 创建了这样一个条件渲染器。为了有条件地呈现多个 children,您需要将其包装到一个额外的 DOM 组件中。
const ShouldSingleChildUpdate = React.createClass({
shouldComponentUpdate(nextProps) {
return nextProps.children !== this.props.children;
},
render() {
return this.props.children;
},
});
const AlertRenderer = React.createClass({
/* ... */
render() {
return (
<div>
<ShouldSingleChildUpdate>
{this.props.children}
</ShouldSingleChildUpdate>
{this.state.alerts.map(alert =>
<this.props.component
key={alert} // Or whatever uniquely describes an alert
alert={alert}
/>
)}
</div>
);
},
});
请注意,这种方法适用于任何类型的模态框,其中可以随时在屏幕上显示多个模态框。在您的情况下,由于您在任何时候都应该只在屏幕上显示一个 Alert
组件,因此您可以通过仅在状态中存储一个警报来简化 AlertRenderer
代码。