Created
September 23, 2017 22:21
-
-
Save adamrneary/8b0ed25c512345597ac3fc4c64900130 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint react/forbid-foreign-prop-types: 1 */ | |
import React, { Component } from 'react'; | |
import PropTypes from 'prop-types'; | |
import { PageName } from 'airbnb-jitney-schemas/logging_type_page_name_v1'; | |
import AirbnbInteractiveLogger, { | |
NEVER_INTERACTIVE, | |
VALIDATED_AIRBNB_INTERACTIVE, | |
} from '../utils/AirbnbInteractiveLogger'; | |
export const withAirbnbInteractivePropTypes = { | |
initializeAirbnbInteractive: PropTypes.func, | |
interactive: PropTypes.bool, | |
}; | |
const childContextTypes = { | |
pageName: PropTypes.oneOf(Object.values(PageName)), | |
}; | |
/** | |
* HOC that passes two important props to the wrapped component: | |
* - initializeAirbnbInteractive (func) | |
* - interactive (bool) | |
* | |
* `initializeAirbnbInteractive` should be called in the constructor of the wrapped component. | |
* initializeAirbnbInteractive({ | |
* pageName: PageName.PdpHomeLuxury, | |
* isInteractive: () => !!this.props.listing, | |
* info: { filters: { guests: 3 } }, | |
* }); | |
* | |
* The function logs two events: | |
* - Impression fires as early as possible and contains no performance data | |
* - Time To Interactive (TTI) is our primary performance measurement. | |
* | |
* Arguments for initializeAirbnbInteractive: | |
* - pageName (required value of airbnb-jitney-schemas/logging_type_page_name_v1) | |
* - isInteractive (optional function) bound to the wrapped component, defines conditions in | |
* which the component is interactive. If nothing is specified (this is rare), TTI fires on | |
* componentDidMount. If specified, it is evaluates on componentDidMount and | |
* componentDidUpdate until the component registers as interactive | |
* | |
* The interactive prop passed to the wrapped component indicates if the component has yet | |
* triggered an interactive state. This prop can be used to hide content below the fold or defer | |
* expensive but non-critical actions. | |
* | |
*/ | |
export default function withAirbnbInteractive({ | |
confirmIsInteractive, | |
extraEventData = {}, | |
universalPageName, | |
}) { | |
if (!universalPageName) { | |
throw new Error('AirbnbInteractive: Cannot log without page name provided.'); | |
} | |
if (!Object.values(PageName).includes(universalPageName)) { | |
throw new Error(`AirbnbInteractive: Cannot log with invalid page name: ${universalPageName}.`); | |
} | |
AirbnbInteractiveLogger.logImpression({ universalPageName, extraEventData }); | |
return function withAirbnbInteractiveHOC(WrappedComponent) { | |
class WithAirbnbInteractive extends Component { | |
constructor(props) { | |
super(props); | |
this.state = { | |
isInteractive: false, | |
}; | |
this.wasSetInteractive = false; | |
this.setInteractive = this.setInteractive.bind(this); | |
this.startPageTransition = this.startPageTransition.bind(this); | |
} | |
getChildContext() { | |
return { | |
universalPageName, | |
}; | |
} | |
componentDidMount() { | |
const { history } = this.props; | |
if (history) { | |
this.unlistenHistory = history.listen((location, action) => { | |
if (action === 'PUSH' || action === 'POP') { | |
this.startPageTransition({ universalPageName, extraEventData }); | |
} | |
}); | |
} | |
this.setInteractive(); | |
} | |
componentDidUpdate() { | |
if (!this.state.isInteractive) { | |
this.setInteractive(); | |
} | |
} | |
componentWillUnmount() { | |
if (!this.state.isInteractive) { | |
AirbnbInteractiveLogger.logAirbnbInteractive({ | |
methodology: NEVER_INTERACTIVE, | |
universalPageName, | |
extraEventData, | |
}); | |
} | |
if (this.timeout) { | |
window.clearTimeout(this.timeout); | |
} | |
if (this.unlistenHistory) { | |
this.unlistenHistory(); | |
} | |
} | |
setInteractive() { | |
const { isInteractive } = this.state; | |
if (this.wasSetInteractive || !confirmIsInteractive()) { | |
return; | |
} | |
// There is a time gap between when we log interactive and when we | |
// setState to interactive. So keep track of when we've logged so we don't | |
// double count. | |
this.wasSetInteractive = true; | |
this.timeout = setTimeout(() => { | |
AirbnbInteractiveLogger.logAirbnbInteractive({ | |
methodology: VALIDATED_AIRBNB_INTERACTIVE, | |
extraEventData, | |
universalPageName, | |
}); | |
this.timeout = setTimeout(() => { | |
this.timeout = null; | |
// For any particular instance of this HOC, we only set `interactive` | |
// to true ONCE. This is important because if there's a page | |
// transition on the same page, we want to log the interactive time | |
// for it (so we need to keep track of when interactivity was reset | |
// via `this.wasSetInteractive` but we don't actually want to change | |
// the value of the `interactive` prop because this would cause | |
// things that have already rendered post-interactive to unrender | |
// then re-render (e.g. Google Maps) which is bad. | |
if (!isInteractive) { | |
this.setState({ isInteractive: true }); | |
} | |
}); | |
}); | |
} | |
startPageTransition() { | |
this.wasSetInteractive = false; | |
AirbnbInteractiveLogger.logStartPageTransition(); | |
} | |
render() { | |
return ( | |
<WrappedComponent | |
{...this.props} | |
interactive={this.state.interactive} | |
initializeAirbnbInteractive={this.initializeAirbnbInteractive} | |
/> | |
); | |
} | |
} | |
const wrappedComponentName = | |
WrappedComponent.displayName || WrappedComponent.name || 'Component'; | |
// eslint-disable-next-line react/forbid-foreign-prop-types | |
if (WrappedComponent.propTypes) { | |
WithAirbnbInteractive.propTypes = { | |
// eslint-disable-next-line react/forbid-foreign-prop-types | |
...WrappedComponent.propTypes, | |
}; | |
} | |
if (WrappedComponent.defaultProps) { | |
WithAirbnbInteractive.defaultProps = { | |
...WrappedComponent.defaultProps, | |
}; | |
} | |
WithAirbnbInteractive.displayName = `WithAirbnbInteractive(${wrappedComponentName})`; | |
WithAirbnbInteractive.childContextTypes = childContextTypes; | |
return WithAirbnbInteractive; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment