Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access React component from this.props.children with react-router

I have a website with the following react-router and react-router-relay setup:

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>

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):

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>
}

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:

  1. User saves edits to a task in <TaskEdit>.
  2. Edits are saved and user taken back to <TaskList>.
  3. Immediately after, I want <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.

like image 323
Chris Avatar asked Apr 25 '16 06:04

Chris


People also ask

How do I access router props in react?

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'.

Can you pass props through react router?

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.

How do I get child props from components?

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.

How do you pass props to a component rendered by react router v6?

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 .


2 Answers

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

like image 107
plus- Avatar answered Sep 28 '22 10:09

plus-


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.

like image 43
Alexandre Kirszenberg Avatar answered Sep 28 '22 09:09

Alexandre Kirszenberg