Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 Router.onUpdate removed #4278

Closed
baronnoraz opened this issue Dec 13, 2016 · 13 comments

Comments

10 participants
@baronnoraz
Copy link

commented Dec 13, 2016

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?

@smashercosmo

This comment has been minimized.

Copy link

commented Dec 13, 2016

I think you can just subscribe directly to history

@pshrmn

This comment has been minimized.

Copy link
Collaborator

commented Dec 13, 2016

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.

@timdorr timdorr closed this Dec 13, 2016

@baronnoraz

This comment has been minimized.

Copy link
Author

commented Dec 14, 2016

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?

@baronnoraz

This comment has been minimized.

Copy link
Author

commented Dec 14, 2016

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.

@smashercosmo

This comment has been minimized.

Copy link

commented Dec 14, 2016

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

@pshrmn

This comment has been minimized.

Copy link
Collaborator

commented Dec 14, 2016

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.

@ahmedelgabri

This comment has been minimized.

Copy link

commented Jan 15, 2017

Maybe you can do something like this?

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

This comment has been minimized.

Copy link

commented Jan 17, 2017

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>
    );
}
@baronnoraz

This comment has been minimized.

Copy link
Author

commented Feb 8, 2017

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’t 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>
);
@gouegd

This comment has been minimized.

Copy link

commented Mar 24, 2017

@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).

@atomicleopard

This comment has been minimized.

Copy link

commented Apr 19, 2017

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>
@juliaqiuxy

This comment has been minimized.

Copy link

commented May 7, 2017

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'),
);
@dangodev

This comment has been minimized.

Copy link

commented Oct 18, 2017

@juliaqiuxy’s solution didn’t work for me using service workers & SSR. withTracker was firing, but GA wasn’t 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’s componentDidMount() callback that fired on every route solved the problem for me. This seemed to work even with SSR & service worker caching.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 18, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.