React Router Link doesn't work with LeafletJS

2020-06-18 12:49发布

问题:

Versions:

  • react-router-dom 4.1.1
  • react-router-redux 5.0.0-alpha.4
  • react-leaflet 1.1.3
  • leaflet 1.0.3

Steps to reproduce

I create a leaflet map. In which I add some markers. These markers have popups. In each of these popup I want to have a <Link>

Also if it helps this is my Routing config:

ReactDOM.render(
  <Provider store={store}>
    <div>
      <AppContainer />
      <ConnectedRouter history={history}>
        <div>
          <MenuContainer />
          <Switch>
            <Route path='/:area/:sport/list' component={ListContainer} />
            <Route path='/:area/:sport/map' component={MapContainer} />
            <Route path='/:area/:sport/rasp' component={RaspContainer} />
            <Route path='/:shortcode/details' component={StationDetailsContainer} />
            <Redirect exact from='/' to='/wellington/paragliding/list' />
            <Route component={NoMatch} />
          </Switch>
        </div>
      </ConnectedRouter>
    </div>
  </Provider>,
  document.getElementById('root')
)

Expected Behavior

I can see my link and click on it when popup opens.

Actual Behavior

Impossible to see the link. It's not generated.

Extra details

Inside my <MapMode> I use <Map> from leaflet. If I set a <Link> just above the <Map> tag it works. As soon as I want to have a link inside my <Map>, somehow it breaks. This is the React structure of my page, <Popup> tag just contains null as Javascript is breaking:

It's quite a complex problem so feel free to ask me questions. Thanks.

回答1:

I'm not 100% sure about this answer. But anyway I'm going to try because I think at least it might shed some light to anyone who will try to solve this problem in future.

I got the first hint from this issue in react-leaflet GitHub repo. According to that and your error, it seems the problem is Popup can't access the router from the context because context isn't passed into the Popup with the way they render it. So we should be able to fix the problem if we can explicitly pass the context to Popup.

Then I found a way to explicitly pass the context into a component in this StackOverflow answer. With that, I think you should be able to use a HoC(Higher order Component) as follows to solve your problem.

This is the HoC that inject context to a component:

function withContext(WrappedComponent, context){

  class ContextProvider extends React.Component {
    getChildContext() {
      return context;
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }

  ContextProvider.childContextTypes = {};
  Object.keys(context).forEach(key => {
    ContextProvider.childContextTypes[key] = React.PropTypes.any.isRequired; 
  });

  return ContextProvider;
}

Let's say you are using Popup inside a component called MapMaker. Then you can inject the context with router into Popup using the HoC like this.

class MapMaker extends React.Component {

  //......

  // This make sure you have router in you this.context
  // Also you can add any other context that you need to pass into Popup
  static contextTypes = {
    router: React.PropTypes.object.isRequired
  }

  render(){

    const PopupWithContext = withContext(Popup, this.context);

    return (
      //..... your JSX before Popup
      <PopupWithContext/> // with your props
      //..... your JSX after Popup
    );
  }

}


回答2:

I tried the solution suggested by Tharaka but it didn't work for me. It looks like react-leaflet's Popup is using it's own context, thus blocking context that is passed from higher levels. However, inspired by this solution I came up with another one, really simple & based on the composition principal.

I created RouterForwarder component

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class RouterForwarder extends Component {
  getChildContext() {
    return this.props.context
  }

  render() {
    return <span>{this.props.children}</span>
  }
}

RouterForwarder.childContextTypes = {
  router: PropTypes.object.isRequired,
}

RouterForwarder.propTypes = {
  context: PropTypes.object.isRequired,
}

export default RouterForwarder

and then used it in my component (the one that renders Map, Marker, Popup & Link) in the following way:

import RouterForwarder from './RouterForwarder'

class MyComponent extends Component {

  render() {
    return (
    ...
      <Popup>
        <RouterForwarder context={this.context}>
          <Link to={'my destination'}>Go to My Destination</Link>
        </RouterForwarder>
      </Popup>
    ...)
  }
}

MyComponent.contextTypes = {
  router: PropTypes.object,
}


回答3:

My solution (it's a workaround but works great and I see no downsides):

I solved by using (in react router v3 with redux)

<a onClick={() => goTo(params)} />

whereas goTo is defined in

const mapDispatchToProps = (dispatch) => bindActionCreators({
  goTo(id) {
    return push(`/<url>/${id}`); // push from 'react-router-redux'
  },
}, dispatch);