withTracker not being reactive to path changes on React-Router


#1

I’m fairly new to using React in Meteor, and part of the routing is causing me a lot of grief using React Router. I can render all of my pages, but I can’t get my withTracker container to react to changes in the route. I think it’s partially because of my structure, but it seems like there should be a good way to make this work.

My basic structure is that I have a layout component inside of a React-Router Router. I pass a content function to this array, with a Switch and Routes. I also have a withTracker container wrapping the Router. I handle my subscriptions in there…and it doesn’t seem to be reactive. I can’t get it to re-run the subscription on a path change.

class RenderRoutes extends Component {
    static propTypes = {
        locations: PropTypes.object,
        contents: PropTypes.func
    }

    render(){
        return (
            <Router history={browserHistory}>
                <Layout
                    locations={this.props.locations}
                    contents={this.props.contents}
                />
            </Router>
        );
    }
}


export default RenderRoutesContainer = withTracker((props)=>{
    const [site_id, bldg_id, space_id] = window.location.pathname.split('/').slice(2);

    const location_subscriptionHandle = Meteor.subscribe('location_basics', site_id, bldg_id, space_id);
    const locations = location_subscriptionHandle.ready() ? {
        sites: _Sites.find().fetch(),
        bldgs: _Bldgs.find({'localID.site_id': site_id}).fetch(),
        spaces: _Spaces.find({'localID.site_id': site_id, 'localID.bldg_id': bldg_id}).fetch()
    } : {sites:[], bldgs:[], spaces:[]};
    const contents = Contents;

    return {
        locations: locations,
        contents: contents
    }
})(RenderRoutes);

function Contents(){
    return(
        <Switch>
            <Redirect exact from="/" to="/home" />
            <Route exact path="/home/:site_id?/:bldg_id?/:space_id?" component={Home}/>

            <Route exact path="/sites/:site_id?/:bldg_id?/:space_id?" component={Sites}/>
            <Route exact path="/buildings/:site_id?/:bldg_id?/:space_id?" component={Buildings}/>
            <Route exact path="/rooms/:site_id?/:bldg_id?/:space_id?" component={Rooms}/>

            <Route component={NoMatch} />
        </Switch>
    )
}

#2

One thing that I do notice is that you are using the window.location instead of the params prop. I would try rewriting your container like so.

export default RenderRoutesContainer = withTracker(({ params })=>{
    const { site_id, bldg_id, space_id } = params;

    const location_subscriptionHandle = Meteor.subscribe('location_basics', site_id, bldg_id, space_id);
    const locations = location_subscriptionHandle.ready() ? {
        sites: _Sites.find().fetch(),
        bldgs: _Bldgs.find({'localID.site_id': site_id}).fetch(),
        spaces: _Spaces.find({'localID.site_id': site_id, 'localID.bldg_id': bldg_id}).fetch()
    } : {sites:[], bldgs:[], spaces:[]};
    const contents = Contents;

    return {
        locations: locations,
        contents: contents
    }
})(RenderRoutes);

#3

I gave it a try, but for some reason params comes back undefined


#4

I figured it out! The trouble was that I was wrapping the entire Router in the withTracker, so it didn’t get things like the location prop.

This is still feels slightly jenky, but it works for what I need it to do as a top-level layout. Thanks for the feedback, it helped me get to this point!

export default class MainRouter extends Component{
    render(){
        return(
            <Router history={browserHistory}>
                <Route component={RenderRoutesContainer}/>
            </Router>
        )
    }
}

class RenderRoutes extends Component {
    static propTypes = {
        locations: PropTypes.object,
        contents: PropTypes.func
    }

    render(){
        return (
                <Layout
                    locations={this.props.locations}
                    contents={this.props.contents}
                />
        );
    }
}


const RenderRoutesContainer = withTracker(( props )=>{
    const [site_id, bldg_id, space_id] = props.location.pathname.split('/').slice(2);

    const location_subscriptionHandle = Meteor.subscribe('location_basics', site_id, bldg_id, space_id);
    const locations = location_subscriptionHandle.ready() ? {
        sites: _Sites.find().fetch(),
        bldgs: _Bldgs.find({'localID.site_id': site_id}).fetch(),
        spaces: _Spaces.find({'localID.site_id': site_id, 'localID.bldg_id': bldg_id}).fetch()
    } : {sites:[], bldgs:[], spaces:[]};
    const contents = Contents;

    return {
        locations: locations,
        contents: contents
    }
})(RenderRoutes);

function Contents(){...}

#5

Yes, I was actually thinking about this issue a bit ago and realized that the params and location props are passed from the Route component so they wouldn’t be available at this level.