How to use React.Context for event tracking - javascript

I'm trying to create a generic event tracking component for my react app. My general idea was to have a global event listener that would use html attributes to track events and trigger events handlers (click, submit etc) based on the event that was fired. I wanted to use React.Context api to be able to track page level data to use for tracking purposes.
The problem I'm having is figuring out how to use React.Context api is in this use case. Here's the basic structure I figured I would be using:
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Switch, Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './lib/store'
import history from './lib/history'
import Login from './Containers/Login/Login'
import Logout from './Containers/Login/Logout'
import Hello from './Containers/Hello/Hello'
import AnalyticsProvider from './lib/tracking/AnalyticsProvider'
ReactDOM.render((
<Provider store={store}>
<AnalyticsProvider>
<Router history={history}>
<Switch>
<Route path={'/login'} component={Login} />
<Route path={'/logout'} component={Logout} />
<Route path={'/hello'} component={Hello} />
</Router>
</AnalyticsProvider>
</Provider>
), document.getElementById('root'))
lib/tracking/AnalyticsProvider.js
import React from 'react'
class AnalyticsProvider extends React.Component {
componentDidMount() {
_initClickHandler()
_initSubmitHandler()
}
_initClickHandler = () => {
window.addEventListener('click', _handleClick, false)
}
_initSubmitHandler = () => {
window.addEventListener('submit', _handleSubmit, false)
}
_handleClick = () => {
// Handle adding click event to analytics manager here
console.log('Click Event fired')
}
_handleSubmit = () => {
// Handle adding submit event to analytics manager here
console.log('Submit event fired')
}
render = () => this.props.children
}
export default AnalyticsProvider
Containers/Hello/Hello
import React from 'react'
class Hello extends React.Component {
constructor(props) {
this.state = {
isHidden: true,
testData: 'Some sample data'
}
}
render() {
return (
<div className="hello-component">
{ this.state.isHidden ? '' : 'Hellow'}
<button data-analytics-name="TEST_HELLO" onClick={ () => this.setState({isHidden: !this.state.isHidden})) }>Toggle Hello</button>
</div>
);
}
}
So, conceptually in the _handleClick method I want to be able to access context data from the Hello compoent when the 'Toggle Hello' button is clicked.
I can't figure out how to set up my components this way. Any thoughts? Or in general any better approaches to generic analytics capturing?

What context data are you trying to access from Hello component? Is it data-analyticsName? You can easily access data attribute in AnalyticsProvider like so:
_handleClick = e => {
// Handle adding click event to analytics manager here
if (e.target.dataset.analyticsName) {
console.log(
"Click event fired with data " + e.target.dataset.analyticsName
);
}
};
Here is your code with some small changes: https://codesandbox.io/s/elastic-spence-6fyu1

Related

Push to new route without any further actions on the component

We use an external componet which we don't control that takes in children which can be other components or
used for routing to another page. That component is called Modulation.
This is how we are currently calling that external Modulation component within our MyComponent.
import React, {Fragment} from 'react';
import { withRouter } from "react-router";
import { Modulation, Type } from "external-package";
const MyComponent = ({
router,
Modulation,
Type,
}) => {
// Need to call it this way, it's how we do modulation logics.
// So if there is match on typeA, nothing is done here.
// if there is match on typeB perform the re routing via router push
// match happens externally when we use this Modulation component.
const getModulation = () => {
return (
<Modulation>
<Type type="typeA"/> {/* do nothing */}
<Type type="typeB"> {/* redirect */}
{router.push('some.url.com')}
</Type>
</Modulation>
);
}
React.useEffect(() => {
getModulation();
}, [])
return <Fragment />;
};
export default withRouter(MyComponent);
This MyComponent is then called within MainComponent.
import React, { Fragment } from 'react';
import MyComponent from '../MyComponent';
import OtherComponent1 from '../OtherComponent1';
import OtherComponent2 from '../OtherComponent2';
const MainComponent = ({
// some props
}) => {
return (
<div>
<MyComponent /> {/* this is the above component */}
{/* We should only show/reach these components if router.push() didn't happen above */}
<OtherComponent1 />
<OtherComponent2 />
</div>
);
};
export default MainComponent;
So when we match typeB, we do perform the rerouting correctly.
But is not clean. OtherComponent1 and OtherComponent2 temporarily shows up (about 2 seconds) before it reroutes to new page.
Why? Is there a way to block it, ensure that if we are performing router.push('') we do not show these other components
and just redirect cleanly?
P.S: react-router version is 3.0.0

How to add a watcher for context change?

I am working on adding an analytics tracker to my react app. I want to primarily capture 2 things:
1) All click events.
2) All page change events.
I was trying to figure out how to approach this problem and found some help on SO with this:
How can I create a wrapper component for entire app?
The above post basically had me creating a parent wrapper and using the React Context API to pass data to the nested elements. The idea is great, but I'm still missing a few pieces here after reading the context API.
Heres what I have following that pattern.
Tracker.js
import PropTypes from "prop-types"
import * as React from "react"
import { connect } from "react-redux"
import TrackingManager from './TrackingManager'
import ScriptManager from "./ScriptManager"
import { isLeftClickEvent } from "../utils/Utils"
const trackingManager = new TrackingManager()
export const TrackerProvider = React.createContext()
/**
* Tracking container which wraps the supplied Application component.
* #param Application
* #param beforeAction
* #param overrides
* #returns {object}
*/
class Tracker extends React.Component {
constructor(props) {
super(props)
this.state = {
pageName: ''
}
}
componentDidMount() {
this._addClickListener()
this._addSubmitListener()
}
componentWillUnmount() {
// prevent side effects by removing listeners upon unmount
this._removeClickListener()
this._removeSubmitListener()
}
componentDidUpdate() {
console.log('TRACKER UPDATE')
}
pageLoad = pageName => {
console.log('LOADING PAGE')
this.setState({ pagename }, trackingManager.page(this.state))
}
/**
* Add global event listener for click events.
*/
_addClickListener = () => document.body.addEventListener("click", this._handleClick)
/**
* Remove global event listern for click events.
*/
_removeClickListener = () => document.body.removeEventListener("click", this._handleClick)
/**
* Add global event listener for submit events.
*/
_addSubmitListener = () => document.body.addEventListener("submit", this._handleSubmit)
/**
* Remove global event listern for click events.
*/
_removeSubmitListener = () => document.body.removeEventListener("submit", this._handleSubmit)
_handleSubmit = event => {
console.log(event.target.name)
}
_handleClick = event => {
// ensure the mouse click is an event we're interested in processing,
// we have discussed limiting to external links which go outside the
// react application and forcing implementers to use redux actions for
// interal links, however the app is not implemented like that in
// places, eg: Used Search List. so we're not enforcing that restriction
if (!isLeftClickEvent(event)) {
return
}
// Track only events when triggered from a element that has
// the `analytics` data attribute.
if (event.target.dataset.analytics !== undefined) {
let analyticsTag = event.target.dataset.analytics
console.log("Analytics:", analyticsTag)
trackingManager.event("eventAction", {"eventName": analyticsTag, "pageName": "Something"})
}
}
/**
* Return tracking script.
*/
_renderTrackingScript() {
/**
* If utag is already loaded on the page we don't want to load it again
*/
if (window.utag !== undefined) return
/**
* Load utag script.
*/
return (
<ScriptManager
account={process.env.ANALYTICS_TAG_ACCOUNT}
profile={process.env.ANALYTICS_TAG_PROFILE}
environment={process.env.ANALYTICS_TAG_ENV}
/>
)
}
render() {
return (
<TrackerProvider.Provider value={
{
state: this.state,
loadPage: this.pageLoad
}
}>
{this.props.children}
{this._renderTrackingScript()}
</TrackerProvider.Provider>
)
}
}
export default Tracker
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Switch, Route } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './lib/store'
import history from './lib/history'
import MyComp from './containers/components/MyComp'
import Tracker from './lib/tracking/Tracker'
import './assets/stylesheets/bootstrap.scss'
import './bootstrap-ds.css'
import './index.css'
import './assets/stylesheets/scenes.scss'
ReactDOM.render((
<Tracker>
<Provider store={store}>
<Router history={history}>
<Switch>
<Route path={'/analytics'} component={MyComp} />
</Switch>
</Router>
</Provider>
</Tracker>
), document.getElementById('root'))
MyComp.js
import React from 'react
import { TrackerProvider } from '../../lib/tracking/Tracker
const MyComp = () => {
return (
<TrackerProvider.Consumer>
{context =>
<>
<div>This is my test page for track events for analytics</div>
<button data-analytics="TEST_BUTTON">Test Analytics</button>
</>
}
</TrackerProvider.Consumer>
)
}
export default MyComp
Here's what I'm struggling with a little bit:
1. When I load a nested child component that consumes the context, how do I notify the Parent (<Tracker />) to trigger some function? Similar to componentDidUpdate.
In essence a user navigates to the MyComp page and the pageLoad function is fired in the Tracker.2. How do I update the Context from MyComp without depending on some click event in the render method to run a funciton. So maybe in componentDidUpdate I can update the context.
I noticed you had connect from react-redux. Redux already provides its state to all the components in your app, so if you're already using Redux, you don't need to mess with the context API directly.
It's possible to create a higher-order component (a component that takes a component and returns a component) and attach event listeners to that capable of catching all the click events in your app.
A click disptaching HOC might look something like this:
import React from 'react';
import { useDispatch } from 'react-redux';
import logClick from '../path/to/log/clicks.js';
const ClickLogger = Component => (...props) => {
const dispatch = useDispatch();
return <div onClick={e => dispatch(logClick(e))}>
<Component {...props } />
</div>;
};
logClick will be a Redux action creator. Once you've got your log actions dispatching to Redux, you can use redux middleware to handle your log actions. If you want to hit a tracking pixel on a server or something, you could use redux-saga to trigger the logging effects.
If you want to track every page load, you can create a higher-order component which uses the useEffect hook with an empty array ([]) as the second argument. This will fire an effect on the first render, but no subsequent renders.

Hitting Back button in React app doesn't reload the page

I have a React app (16.8.6) written in TypeScript that uses React Router (5.0.1) and MobX (5.9.4). The navigation works fine and data loads when it should, however, when I click the browser's Back button the URL changes but no state is updated and the page doesn't get re-rendered. I've read endless articles about this issue and about the withRouter fix, which I tried but it doesn't make a difference.
A typical use case is navigating to the summary page, selecting various things which cause new data to load and new history states to get pushed and then going back a couple of steps to where you started. Most of the history pushes occur within the summary component, which handles several routes. I have noticed that when going back from the summary page to the home page the re-rendering happens as it should.
My index.tsx
import { Provider } from 'mobx-react'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
import * as serviceWorker from './serviceWorker'
import * as Utils from './utils/Utils'
const rootStore = Utils.createStores()
ReactDOM.render(
<Provider {...rootStore }>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
)
serviceWorker.unregister()
My app.tsx
import * as React from 'react'
import { inject, observer } from 'mobx-react'
import { Route, Router, Switch } from 'react-router'
import Home from './pages/Home/Home'
import PackageSummary from './pages/PackageSummary/PackageSummary'
import ErrorPage from './pages/ErrorPage/ErrorPage'
import { STORE_ROUTER } from './constants/Constants'
import { RouterStore } from './stores/RouterStore'
#inject(STORE_ROUTER)
#observer
class App extends React.Component {
private routerStore = this.props[STORE_ROUTER] as RouterStore
public render() {
return (
<Router history={this.routerStore.history}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/summary/:packageId" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName" component={PackageSummary} />
<Route exact path="/summary/:packageId/:menuName/:appName" component={PackageSummary} />
<Route component={ErrorPage} />
</Switch>
</Router>
)
}
}
export default App
My router store
import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router'
import { createBrowserHistory } from 'history'
export class RouterStore extends BaseRouterStore {
constructor() {
super()
this.history = syncHistoryWithStore(createBrowserHistory(), this)
}
}
How I create the MobX stores
export const createStores = () => {
const routerStore = new RouterStore()
const packageListStore = new PackageListStore()
const packageSummaryStore = new PackageSummaryStore()
const packageUploadStore = new PackageUploadStore()
return {
[STORE_ROUTER]: routerStore,
[STORE_SUPPORT_PACKAGE_LIST]: packageListStore,
[STORE_SUPPORT_PACKAGE_SUMMARY]: packageSummaryStore,
[STORE_SUPPORT_PACKAGE_UPLOAD]: packageUploadStore
}
}
So my questions are:
How can I get the page to load the proper data when the user goes back/forward via the browser?
If the solution is being able to get MobX to observe changes to the location, how would I do that?
You could implement something like this in your component:
import { inject, observer } from 'mobx-react';
import { observe } from 'mobx';
#inject('routerStore')
#observer
class PackageSummary extends React.Component {
listener = null;
componentDidMount() {
this.listener = observe(this.props.routerStore, 'location', ({ oldValue, newValue }) => {
if (!oldValue || oldValue.pathname !== newValue.pathname) {
// your logic
}
}, true)
}
componentWillUnmount() {
this.listener();
}
}
Problem with this approach is that if you go back from /summary to other page (e.g. '/'), callback will initiate, so you would also need some kind of check which route is this. Because of these kind of complications I would suggest using mobx-state-router, which I found much better to use with MobX.
React router monitors url changes and renders associated component defined for the route aka url.
You have to manually refresh or call a window function to reload.
If I remember correctly, using a browser back function does not reload the page (I might be wrong).
Why not try to detect the back action by a browser and reload the page when detected instead?
You can try the following code to manually reload the page when the browser back button is clicked.
$(window).bind("pageshow", function() {
// Run reload code here.
});
Also out of curiosity, why do you need so many different stores?
In App.js
useEffect(() => {
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
}, []);

Console.Log Not Being Called Inside React Constructor

I'm trying to add a component to a default .NET Core MVC with React project. I believe I have everything wired up to mirror the existing "Fetch Data" component, but it doesn't seem like it's actually being called (but the link to the component in my navbar does move to a new page).
The component itself...
import React, { Component } from 'react';
export class TestComponent extends Component {
static displayName = TestComponent.name;
constructor (props) {
super(props);
console.log("WHO NOW?");
this.state = { message: '', loading: true, promise: null };
this.state.promise = fetch('api/SampleData/ManyHotDogs');
console.log(this.state.promise);
}
static renderForecastsTable (message) {
return (
<h1>
Current Message: {message}
</h1>
);
}
render () {
let contents = this.state.loading
? <p><em>Loading...</em></p>
: TestComponent.renderForecastsTable(this.state.message);
return (
<div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
{contents}
</div>
);
}
}
The App.js
import React, { Component } from 'react';
import { Route } from 'react-router';
import { Layout } from './components/Layout';
import { Home } from './components/Home';
import { FetchData } from './components/FetchData';
import { Counter } from './components/Counter';
import { TestComponent } from './components/TestComponent';
export default class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={Counter} />
<Route path='/fetch-data' component={FetchData} />
<Route path='/test-controller' component={TestComponent} />
</Layout>
);
}
}
That console.log("Who now") is never called when I inspect, and the page remains totally blank. I can't find a key difference between this and the functioning components, and google has not been much help either. Any ideas what is missing?
Edit
While troubleshooting this, I ended up creating a dependency nightmare that broke the app. Since I'm only using the app to explore React, I nuked it and started over--and on the second attempt I have not been able to reproduce the not-rendering issue.
It is advisable to use componentDidMount to make the call to the REST API with the fetch or axios.
class TestComponent extends Component{
constructor(props){
state = {promise: ''}
}
async componentDidMount () {
let promise = await fetch ('api / SampleData / ManyHotDogs');
this.setState ({promise});
console.log (promise);
}
render(){
return(
<div>{this.state.promise}</div>
);
}
}

React Redux Loading bar for react router navigation

So I'd like to implement a loading bar just like github has. It should start loading on a click to another page and finish when it arrived.
I'm using material-ui and for the loader react-progress-bar-plus.
I tried to use react-router's lifecycle hooks, namely componentDidUpdate and componentWillReceiveProps to set the state to be finished.
For start, I attached an onTouchTap function to the menu items but it just does not want to work properly.
What is the best way to implement this feature?
You can use router-resolver with react-progress-bar-plus.
See this example:
http://minhtranite.github.io/router-resolver/ex-4
The usage example:
// app.js
//...
import {RouterResolver} from 'router-resolver';
//...
const routes = {
path: '/',
component: App,
indexRoute: {
component: require('components/pages/PageHome')
},
childRoutes: [
require('./routes/Example1Route'),
require('./routes/Example2Route'),
require('./routes/Example3Route')
]
};
const renderInitial = () => {
return <div>Loading...</div>;
};
const onError = (error) => {
console.log('Error: ', error);
};
ReactDOM.render(
<Router routes={routes}
history={history}
render={props => (
<RouterResolver {...props} renderInitial={renderInitial} onError={onError}/>
)}/>,
document.getElementById('app')
);
And:
// components/pages/PageExample1.js
import React from 'react';
import Document from 'components/common/Document';
class PageExample1 extends React.Component {
static resolve() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('simple data');
}, 2000);
});
};
static propTypes = {
response: React.PropTypes.string.isRequired
};
render() {
return (
<Document title='Example1 | Router resolver' className='page-ex-1'>
<h1>Example 1: {this.props.response}</h1>
</Document>
);
}
}
export default PageExample1;
I made a small package react-router-loading that allows you to show loading indicator and fetch some data before switching the screen.
Just use Switch and Route from this package instead of react-router-dom:
import { Switch, Route } from "react-router-loading";
Add loading props to the Route where you want to wait something:
<Route path="/my-component" component={MyComponent} loading/>
And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:
import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);
const loading = async () => {
//fetching some data
//call method to indicate that fetching is done and we are ready to switch
loadingContext.done();
};

Categories

Resources