How can I create a wrapper component for entire app? - javascript

I'm trying to add some analytics tracking for my react app. Basically just using a component to add global event listeners and then handle the event appropriately in that component.
I want to wrap my entire app in this component and for it to pick up componentWillUpdate prop changes so I can react to page changes using prop.location. My problem is I don't know how to setup my wrapper component to do this. I know the concept of HOC can help wrap one component and I've tested that to work but I want this to be a more generic and global component.
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()
const config = {
useTagManager: true,
tagManagerAccount: 'testCccount',
tagManagerProfile: 'testProfile',
tagManagerEnvironment: 'dev'
}
/**
* compares the locations of 2 components, mostly taken from:
* http://github.com/nfl/react-metrics/blob/master/src/react/locationEquals.js
*
* #param a
* #param b
* #returns {boolean}
*/
function locationEquals(a, b) {
if (!a && !b) {
return true;
}
if ((a && !b) || (!a && b)) {
return false;
}
return (
a.pathname === b.pathname && a.search === b.search && a.state === b.state
);
}
/**
* Tracking container which wraps the supplied Application component.
* #param Application
* #param beforeAction
* #param overrides
* #returns {object}
*/
const track = Application =>
class TrackingContainer extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
this._addClickListener()
this._addSubmitListener()
}
componentWillUnmount() {
// prevent side effects by removing listeners upon unmount
this._removeClickListener()
this._removeSubmitListener()
}
componentDidUpdate(prevProps) {
// if and only if the location has changed we need to track a
// new pageview
if (!locationEquals(this.props.location, prevProps.location)) {
this._handlePageView(this.props)
}
}
_addClickListener = () => {
// bind to body to catch clicks in portaled elements (modals, tooltips, dropdowns)
document.body.addEventListener("click", this._handleClick)
}
_removeClickListener = () => {
document.body.removeEventListener("click", this._handleClick)
}
_addSubmitListener = () => {
document.body.addEventListener("submit", this._handleSubmit)
}
_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) {
trackingManager.event('pageName', 'User')
}
}
_handlePageView = route => {
console.log('CHANGE PAGE EVENT')
console.log(route)
}
/**
* 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
if (config.useTagManager === false) return
/**
* Load utag script.
*/
return (
<ScriptManager
account={config.tagManagerAccount}
profile={config.tagManagerProfile}
environment={config.tagManagerEnvironment}
/>
)
}
render() {
return (
<React.Fragment>
<Application {...this.props} {...this.state} />
{this.props.children}
{this._renderTrackingScript()}
</React.Fragment>
)
}
}
export default track
With my index.js I want to do something similar to this:
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 Loadable from 'react-loadable'
import PageLoader from './components/PageLoader/PageLoader'
import {
DEFAULT_PATH,
LOGIN_PATH,
LOGOUT_PATH,
USER_PATH,
} from './lib/paths'
const Login = Loadable({ loader: () => import('./scenes/Auth/Login' /* webpackChunkName: 'login' */), loading: PageLoader })
const Logout = Loadable({ loader: () => import('./scenes/Auth/Logout'/* webpackChunkName: 'logout' */), loading: PageLoader })
const User = Loadable({ loader: () => import('./scenes/Auth/User'/* webpackChunkName: 'user' */), loading: PageLoader })
import Track from './lib/tracking/Tracker'
import './assets/stylesheets/bootstrap.scss'
import './bootstrap-ds.css'
import './index.css'
import './assets/stylesheets/scenes.scss'
ReactDOM.render((
// This is an example of what I want to accomplish
<Track>
<Provider store={store}>
<Router history={history}>
<Switch>
<Route path={LOGIN_PATH} component={Login} />
<Route path={LOGOUT_PATH} component={Logout} />
<Route path={USER_PATH} component={User} />
</Switch>
</Router>
</Provider>
</Track>
), document.getElementById('root'))
So, basically where the <Track> component can wrap the entire app and still use the props and check if they update. Is there a way to do this? What do I need to change?

Context API seems to be your use case here. You want a decoupled way to share data between components in the same tree. Your wrapper could implement a Provider, and all components that are interest on the shared value will implement a Consumer. HOC and render Props are useful to share stateful logic, not state itself.
const { Provider, Consumer } = React.createContext()
const Wrapper = ({children}) =>{
return(
<Provider value={mySharedValue}>
{children}
</Provider>
)
}
const NestedChildren = () =>{
return(
<Consumer>
{context => <div>{context}</div>}
</Consumer>
)
}
const App = () =>{
return(
<Wrapper>
<Child> <NestedChild /> </Child>
</Wrapper>
)
}

We accomplished something like this with react-capture-metrics.
You provide your analytics API to a top level provider like so:
import { MetricsProvider } from 'react-capture-metrics'
const analytics = {
track: (name, properties) => window.analytics.track(name, properties),
page: (name, properties, category) => window.analytics.page(...(category ? [category, name, properties] : [name, properties]))
}
function App () {
return (
<MetricsProvider analytics={analytics} properties={{ appVersion: pkg.version }}>
// ...the rest of your app
</MetricsProvider>
)
}
Then render a PageView component wherever you want to call analytics.page().
function Page() {
const { PageView } = useMetrics({
variantId,
// ...properties to capture
}, { ready: variantId !== undefined })
return (
<PageView
name="Home"
category="Customer"
ready={/* some useState value perhaps */ }
>
// ...
</PageView>
)
}
You can use ready to delay calling the event until all the properties you want to pass are loaded. Or you can use pageKey to call the event when the user navigates to the same page but with different params.

Related

Use imported function in return method (invalid hook call, react js)

Wassup Guys,
I have a reusable component, that translates keys into a choosen language through keynames as string or a binded var.
Usually I use a tag for this, but because of different reasons I am switching/replacing current translations with {t('...')}.
Here is the code of the component:
import React from 'react';
import { useTranslation as defaultTranslation } from 'react-i18next';
import i18next from 'i18next';
export const useTranslation = (ns = 'common', options) => {
return defaultTranslation(ns, {
useSuspense: false,
...options,
});
};
export const withTranslation = (Component, ns, options) => {
const TranslatedHOC = (props) => {
const translationProps = useTranslation(ns, options);
return <Component {...translationProps} {...props} />;
};
return TranslatedHOC;
};
export const getCurrentLanguage = () =>
i18next.language || localStorage.getItem('language') || 'de-DE';
First of all I define the const for the used imported function:
const {t} = useTranslation();
normal case: importing my component in the file, where I want to use it and add code above.
Code of my component, where I want to replace the Tags.
// Import React Table
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import LocalizedText from '#components/i18n/LocalizedText';
class T extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
pages: null,
loading: true,
};
this.fetchData = this.fetchData.bind(this);
}
fetchData(state, instance) {
this.props.onFetchData(state, instance).then(() => {
this.setState({
loading: false,
});
});
}
render() {
return (
<ReactTable
{...this.props}
previousText={
<LocalizedText textKey="caseoverview.orderproduct.back" />
}
nextText={
<LocalizedText textKey="caseoverview.orderproduct.continue" />
}
loadingText={<LocalizedText textKey="loading" />}
noDataText={<LocalizedText textKey="placeholders.nodata" />}
pageText={
<LocalizedText textKey="reservationcalculator.search.result.page" />
}
ofText={<LocalizedText textKey="from" />}
rowsText={<LocalizedText textKey="rows" />}
className="case-search-table"
/>
);
}
}
export default T;
...
previousText={
<LocalizedText textKey="caseoverview.orderproduct.back" />
}
...
should change to:
...
previousText={
t('caseoverview.orderproduct.back')
}
...
The problem is, that I can't use the code quoted above without getting any issues regarding invalid hook calls. If I move it out somehow, I get errors telling me that my 't' is either not defined or an unexpected token. Could someone help me out? Searched online for solutios without any result.
A hook can only be used in a functional component. You can change this class component to a functional component, or you can use react-i18next's withTranslation HOC to wrap your class component. See https://react.i18next.com/latest/withtranslation-hoc#when-to-use
#kevin asworth answer helped me out.
Using withTranslation with passing t as prop
const {t} = this.props;
inside the render method worked for me.

How to use React.Context for event tracking

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

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.

How to send this.state from a component to relay root container

I want to change my root query parameter based on the this.state.eventid which is a child component, but I have no clue how to get props to relay root container. I started based on relay-starter-kit.
I have React component that has a dropdown menu, and onSelect it setStates for eventId
renderAttend() {
if (this.props.groups != null && this.state.success != true) {
var events = this.props.events.map(function(event){
var boundSelect = () => {this.setState({eventid:event.nodes[0].id})}
return <MenuItem style={{fontSize:20}}eventKey={event.nodes[0].id} onSelect={boundSelect.bind(this)}>{event.nodes[0].properties.summary} / {event.nodes[0].properties.start}</MenuItem>
},this)
var teams = this.props.groups.map(function(team){
var boundSelect = () => {this.setState({teamid:team.nodes[0].id})}
return <MenuItem style={{fontSize:20}}eventKey={team.nodes[0].id} onSelect={boundSelect.bind(this)}>{team.nodes[0].properties.name}</MenuItem>
},this)
return (
<div>
<ButtonGroup>
<DropdownButton style={{padding:"15px",fontSize:20}}title="Events" id="bg-vertical-dropdown-2">
{events}
</DropdownButton>
<DropdownButton style={{padding:"15px",fontSize:20,marginLeft:"5px"}} title="Groups" id="bg-vertical-dropdown-2">
{teams}
</DropdownButton>
</ButtonGroup>
</div>
)
}
}
I want to use this state to somehow change my root query...
my approute...
import Relay from 'react-relay';
export default class extends Relay.Route {
static paramDefinitions = {
eventId: {required: false}
};
static queries = {
Event : () => Relay.QL`query{eventState(eventId:$eventId)}`,
};
static routeName = 'AppHomeRoute';
}
and my app.js
import 'babel-polyfill';
import App from './components/App';
import AppHomeRoute from './routes/AppHomeRoute';
import React from 'react';
import ReactDOM from 'react-dom';
import Relay from 'react-relay';
ReactDOM.render(
<Relay.RootContainer
Component={App}
route= {new AppHomeRoute}
renderLoading={function() {
return <div style= {{display:"flex",justifyContent:"center",marginTop:"55px"}}> <h1>Loading...</h1></div>;
}}
renderFailure={function(error, retry) {
return (
<div>
<h1>Click Refresh</h1>
</div>
);
}}
/>,
document.getElementById('root')
);
Now I want to this.state.eventid from the react component to update my root query, but I have no idea how to pass data from child component to react root.container. I do not want to use react-router for this :)
p.s. this.props.events were passed to me by an ajax call so they are not saved in relay/graphql data.
For such a case, the better thing to do is to wrap your root query into a story like
{
store {
events(eventId:$eventId)
}
}
So in the root query you only have
export default class extends Route {
static queries = {
app:() => Relay.QL`query { store }`
};
static routeName = "AppRoute";
}
And in the page you create a fragemnt like
let RelayApp = createContainer(SomeComponent, {
initialVariables: {
eventId: null
},
fragments: {
app: () => Relay.QL `
fragment on Store {
id
events(eventId: $eventId) {
pageInfo {
hasNextPage
}
edges {
cursor
node {
name
...
}
}
}
}
`,
},
});
export
default RelayApp;
For the child component, you set the eventId and onChange event handler as props from parent component. And in the parent componet you implement the event handler and call this.props.setVariables({eventId: someVal}) like
// Child Component
export default class Menu extends Component {
render() {
return(
<ul>
<li onClick={() => this.props.selectItem(val)}>{val}</li>
...
</ul>
)
}
}
// Parent Component
class Main extends Component {
_selectItem = (val) => {
this.props.relay.setVariables({eventId: val});
}
render() {
return(
<div>
<Menu selectItem={() => this._selectItem}/>
</div>
)
}
}
let RelayApp = ...
export default Main
Hope this will help.
There is no easy way to solve this. Either use react-router-relay or nest your query like this and use this.props.relay.setVariables()
viewer {
eventState(eventid:$eventid) {
data
}
}

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