React-router: v4 Router.onUpdate removed

Created on 13 Dec 2016  路  13Comments  路  Source: ReactTraining/react-router

First, my apologies. I'm not sure if this is a bug or a purposeful change for v4. I was looking to integrate react-ga (a google analytics component), which would typically hook into the onUpdate function on the Router.

However, it appears that the onUpdate is missing or has been removed in v4. Assuming the latter, what is the recommended approach to hooking into the router change with v4?

Most helpful comment

With the latest changes to v4 (4.0.0-beta.5), I was able to do the following. I'll be interested to see where v4 ends up on this. I still like listening to the history object directly.

const logPageView = () => {
    ReactGA.set({ page: window.location.pathname });
    ReactGA.pageview(window.location.pathname);
    return null;
};

const NoMatch = () => (
    <div>
        <h2>Whoops</h2>
        <p>Sorry but {location.pathname} didn鈥檛 match any pages</p>
    </div>
);

const App = () => (
    <Router>
        <div>
            <Route path="/" component={logPageView} />
            <Switch>
                <Route exact path="/" component={Home} />
                <Route exact path="/about" component={About} />
                <Route component={NoMatch} />
            </Switch>
        </div>
    </Router>
);

All 13 comments

I think you can just subscribe directly to history

onUpdate was just being called whenever the history subscriber was updating the <Router> with the new location.

In v4, the <StaticRouter> (used internally, not something that you render yourself) includes a context.router.subscribe function that takes functions to be called whenever it receives a new location. You can render a component that subscribes to this and makes any analytics calls whenever it is updated.

For reference, you can check out the <Match> source to see how it subscribes to location changes.

@smashercosmo the history isn't exposed directly unless you create your own. Earlier versions of v4 included it on the context, but that was removed with the latest alpha.

I really appreciate the help gang. I had thought of using history as @smashercosmo suggested, however as @pshrmn points out I couldn't get a reference to it anywhere.

What confuses me about the example is how it has access to the <StaticRouter>.

Here's a condensed example:

App.js

const App = () => (
    <BrowserRouter>
        <GoogleAnalytics/>
    </BrowserRouter>
);

GoogleAnalytics.js

class GoogleAnalytics extends React.Component {
    static contextTypes = {
        router: PropTypes.object
    };
    constructor(props, context) {
        super(props, context)
    }
    componentDidMount() {
        this.unlisten = this.context.router.subscribe(() => {
            console.log("was I successful");
        })
    }
    componentWillUnmount() {
        this.unlisten()
    }    
    render() {
        return null;
    }
}

When the component mounts, this.context.router is a <BrowserRouter> and not a <StaticRouter>. I get an error that this.context.router.subscribe is not a function.

Clearly <Match> is working, so I've obviously missed something important. Any thoughts or suggestions on what dumb thing I did?

Well, I have a solution of sorts, but it feels mighty wrong.

App.js

const App = () => (
    <BrowserRouter>
        <Match pattern="/" component={GoogleAnalytics} />
    </BrowserRouter>
);

GoogleAnalytics.js

const GoogleAnalytics = () => {
    console.log("page change to " + window.location.pathname);
    return null;
};

It appears to work, so I guess I'm happy, but I don't like it. If anyone has suggestions on how to do this better, I am all ears. Thanks again to the React/React Router community for all your help.

I would still use history package directly for this.

import createHistory from 'history/createBrowserHistory'
const history = createHistory()
history.listen((location, action) => {
  /** your analytics logic */
});

You don't need router for this

I just realized earlier today that the most recent version of v4 that includes context.router.subscribe hasn't been released, so you can't take advantage of that yet.

Maybe you can do something like this?

<Match exactly pattern='/foo' render={props => {
  // your analytics logic here?
  return <Component {...props} />
}} />

I've been doing something like this to watch route changes, seems to be working ok in v4 unless I am missing something:

// Component - Analytics.js
// @flow
import React from 'react';

export default class Analytics extends React.Component {

    static contextTypes = {
        history: React.PropTypes.object,
    };

    componentDidMount() {
        this.context.history.listen((state:Object) => {
            console.log(`I'm now on ${state.pathname}`);
            // do something interesting
        });
    }

    render() {
        return null;
    }
}

Then out with the main routes:

render() {
    const { className } = this.props;
    const { language } = this.state;

    return (
        <Router>
            <div className={classNames('App', className)}>
                <Analytics />
                <Redirector />
                <Nav currentLanguage={language} switchLanguage={this.switchLanguage} />
                <LandingRoutes />
                <DashboardRoutes />
                <ProfileRoutes />
                <Miss component={Page404} />
                <Footer />
            </div>
        </Router>
    );
}

With the latest changes to v4 (4.0.0-beta.5), I was able to do the following. I'll be interested to see where v4 ends up on this. I still like listening to the history object directly.

const logPageView = () => {
    ReactGA.set({ page: window.location.pathname });
    ReactGA.pageview(window.location.pathname);
    return null;
};

const NoMatch = () => (
    <div>
        <h2>Whoops</h2>
        <p>Sorry but {location.pathname} didn鈥檛 match any pages</p>
    </div>
);

const App = () => (
    <Router>
        <div>
            <Route path="/" component={logPageView} />
            <Switch>
                <Route exact path="/" component={Home} />
                <Route exact path="/about" component={About} />
                <Route component={NoMatch} />
            </Switch>
        </div>
    </Router>
);

@baronnoraz nice ! Here are a few notes some might find useful.

<Route path="/" component={logPageView} />
can be simplified into
<Route component={logPageView} />
which is really <logPageView /> wrapped by the withRouter HOC. But you don't use the HOC-provided props, so you can just do <logPageView />.

Also, logPageView is used as a functional stateless React compo. I avoid side-effects in the render function in general, so for those who are like me, we can rely on the React lifecycle hooks instead. I use the recompose library to keep it small (I use it throughout my app - don't take it just for this :)). I have a Layout component as the child of Router that I wrap with lifecycle methods, so this gives:

const logPageView = () => {
  ReactGA.set({ page: window.location.pathname });
  ReactGA.pageview(window.location.pathname);
};

// Layout is my top-level child of Router - so it will be updated on location change
const LayoutWithAnalytics = lifecycle({
  componentWillMount: logPageView,
  componentWillUpdate: logPageView,
})(Layout);

<Router>
        <LayoutWithAnalytics>
          ...
        </LayoutWithAnalytics>
</Router>

Now I wonder if the re-rendering would be triggered by anything else than a Router action. If so, beware, we will have duplicate analytic events (not sure what google analytics does with those, it may ignore them).

Thanks @gouegd - very helpful. An onChange even in react-router would have made this a lot easier - i'm still unsure if this will track page views for changes that aren't location related. This component may require tracking previous vs current state to protect against this.

For anyone else

const track = () => track.page(window.location.pathname);
class TrackPageView extends React.Component {
    componentWillMount() { track() }
    componentWillUpdate() { track() }
    render() { return <Route children={this.props.children}/> }
}

and use it like this:

<BrowserRouter> <TrackPageView> <Switch> ...routes as normal </Switch> </TrackPageView> </BrowserRouter>

I would say a more functional approach here is to create a HOC and wrap your entire app in it. I feel like accessing the pathname via props is much cleaner than window. Here's what I do:

import React from 'react';
import GoogleAnalytics from 'react-ga';

GoogleAnalytics.initialize('UA-0000000-0');

const withTracker = (WrappedComponent) => {
  const trackPage = (page) => {
    GoogleAnalytics.set({ page });
    GoogleAnalytics.pageview(page);
  };

  const HOC = (props) => {
    const page = props.location.pathname;
    trackPage(page);

    return (
      <WrappedComponent {...props} />
    );
  };

  return HOC;
};

export default withTracker;

import withTracker from './withTracker';

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Route component={withTracker(App)} />
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root'),
);

@juliaqiuxy鈥檚 solution didn鈥檛 work for me using service workers & SSR. withTracker was firing, but GA wasn鈥檛 being contacted. Possibly could have had something to do with the way webpack compiled that withTracker function.

Moving that out of there, and importing/calling trackPage() within my App鈥檚 componentDidMount() callback that fired on every route solved the problem for me. This seemed to work even with SSR & service worker caching.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nicolashery picture nicolashery  路  3Comments

sarbbottam picture sarbbottam  路  3Comments

jzimmek picture jzimmek  路  3Comments

ArthurRougier picture ArthurRougier  路  3Comments

alexyaseen picture alexyaseen  路  3Comments