I have a website with the following react-router
and react-router-relay
setup:
<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>
The site is a virtual classroom, in which the teacher can create tasks for the students. These tasks can also be edited.
When a user edits a task in TaskEdit
and hits save, the changes are applied and the user redirected to TaskList
. Now I want to give the user a "Your changes were saved!" message once a task was edited.
I have created a bootstrap alert component (<Alert>
) for this exact purpose.
For illustration purposes, assume I have the following mainView
component (check Router above):
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>
}
Normally, I would create this alert component outside of the active component and just pass some kind of function that displays the alert as a prop to the active component. Similar to what is shown here.
However, this only works for a specific component. But because <Alert>
sits at the root-level of my page (along with menubar, sidebar, and other things that are static on all pages), I cannot pass this as a prop due to {this.props.children}
.
Allow me to illustrate the flow I want to achieve once more:
<TaskEdit>
.<TaskList>
.<Alert>
to trigger, and show a message.I've seen other solutions suggesting that you can clone all children of a React element and apply the prop needed to open the alert
, but because of the nature of react-router
this doesn't really seem to work.
How can I accomplish the above? Is there a way for a component to be globally accessible? Please note that I don't want to recreate <Alert>
in each component. Only one such component for the whole DOM.
Another way to access the router props is to add an import statement at the top of the component and import 'withRouter'. import { withRouter } from 'react-router-dom'; Then in the export default statement at the end of the component you would wrap the component in 'withRouter'.
However, with React Router v6, since you're in charge of creating the element, you just pass a prop to the component as you normally would.
To pass data from a child component to its parent, we can call a parent function from the child component with arguments. The parent function can be passed down to the child as a prop, and the function arguments are the data that the parent will receive.
If you need to access the v6 versions of these objects you will use the React hooks, useNavigate for a navigate function which replaced the history object, useParams for params instead of match. params , and useLocation for location .
Simple answer:
Put your Alert component once in your top level view, then use some pubsub or event bus lib to update it.
In your Alert component onComponentDidMount function you listen for a specific event and data and update its state accordingly (visibility and message)
In your Task save function you publish your event to the event bus.
More advance answer:
Use a React flux library https://facebook.github.io/flux/docs/overview.html
A simple solution would be to use the 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} />
},
});
However, this ties into an imperative API. It also means that all your children components that spawn alerts must re-implement a bit of boilerplate.
You could also move everything that relate to Alert rendering into their own components, hiding your implementation under a simpler 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;
},
});
Your application's code then becomes:
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>
);
},
});
An issue with this particular approach is that AlertRenderer will re-render all of its children whenever an alert is updated. This can cause infinite recursion as AlertSub's componentWillReceiveProps lifecycle fires again. Note that this problem only appears if your components have mutable props and don't implement shouldComponentUpdate
.
A simple way to fix this would be to create a conditional children renderer, that only updates when it detects that its children have changed.
The example code below creates such a conditional renderer for a single child. In order to conditionally render multiple children, you'd need to wrap then into an additional DOM component.
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>
);
},
});
Note that this approach was thought for modals of any kind, where more than one modals can be displayed on screen at any time. In your case, since you should only ever have one Alert
component on screen at any time, you can simplify the AlertRenderer
code by only storing a single alert in state.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With