How to add a watcher for context change? - javascript

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.

Related

How can I create a wrapper component for entire app?

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.

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

Jest expected mock not called (redux component)

In React, I am testing that a button click inside a child component causes a function to be called in the parent component (onDeleteClick), via event bubbling.
For this test, I am using mount, as shallow will not allow us to trigger a function in a child component.
onDeleteClick, the function I am trying to check whether it was called or not, is a class property which in this case, is an arrow function.
I am mocking the onDeleteClick function, and passing it into my component via a Redux Provider when starting the test.
The problem I am having is that at the end of the test, when I perform a check to see if the mocked function was called, it returns 0.
expect(onDeleteClick.mock.calls.length).toBe(1);
If I put a console.log within onDeleteClick(), it's outputted during the test, so I know that the function is in fact being called.
I have researched this quite a bit and so far haven't gotten anything to work.
Some suggestions were to spy on my mocked function, and then call forceUpdate on the wrapper, but this didn't yield any positive results.
For this, I am using Jest with Enzyme.
Reference Code:
Parent.js
import { deleteAccount } from '../../actions/profileActions';
import ChildComponent from '../common/ChildComponent';
class ParentComponent extends Component {
onDeleteClick = () => {
console.log('onDeleteClick was executed during this test!')
this.props.deleteAccount();
}
render() {
let dashboardContent;
dashboardContent = (
<div>
<ChildComponent onDelete={this.onDeleteClick} />
</div>
);
return (
<div>
{dashboardContent}
</div>
);
}
}
// propTypes and mapStateToProps removed from this post
export default connect(
mapStateToProps,
{ deleteAccount }
)(ParentComponent);
__tests__/ParentComponent.js
import React from 'react';
import { mount } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import ParentComponent from '../ParentComponent';
import thunk from "redux-thunk";
const mockStore = configureStore([thunk]);
const deleteAccount = jest.fn();
const props = {
deleteAccount
}
const randomTestState = {
// some initial state, not important
};
const randomTestStore = mockStore(randomTestState);
describe('<ParentComponent />', () => {
it(`mounts the ParentComponent component and, when ChildComponent sends onDelete, then deleteAccount function is called once`, () => {
const wrapper = mount(
<Provider store={randomTestStore} props={props}>
<Router >
<ParentComponent />
</Router>
</Provider>
);
// Here, I grab an element in ChildComponent and simulate a click using Enzyme, then the event bubbles up, and deleteAccount() is called in the parent component.
// the console.log we expect to see from onDeleteClick is logged to console.
// the call does not seem to have registered though and the expect returns falsy
expect(deleteAccount.mock.calls.length).toBe(1);
})
});
Could the problem be that I am wrapping the component in a Provider?
I have a hunch, but I couldn't find any concrete examples of tests which use a Provider to wrap their component when running integration testing
The solution was that I needed to change my main ParentComponent file from
class ParentComponent extends Component {
to this:
extend class ParentComponent extends Component {
and then in my test file, import the component like so:
import { ParentComponent } from '../ParentComponent'; // non-default export should be wrapped in braces
and then update my test so that I assign the wrapper variable like so:
const wrapper = mount(<ParentComponent {...props} />);
This then allowed the test to pass
expect(deleteAccount.mock.calls.length).toBe(1);
It was recommended here in the Redux docs

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();
}
};
}, []);

Understanding React Higher-Order Components

Can someone please explain Higher-order components in React. I have read and re-read the documentation but cannot seem to get a better understanding. According to the documentation, HOCs help remove duplication by creating a primary function that returns a react component, by passing arguments to that function.
I have a few questions on that.
If HOCs create a new enhanced component, can it be possible not to pass in any component as argument at all?
In an example such as this, which is the higher order component, the Button or the EnhancedButton.
I tried creating one HOC like this:
// createSetup.js
import React from 'react';
export default function createSetup(options) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.testFunction = this.testFunction.bind(this);
}
testFunction() {
console.log("This is a test function");
}
render() {
return <p>{options.name}</p>
}
}
}
// main.js
import React from 'react';
import {render} from 'react-dom';
import createSetup from './createSetup';
render((<div>{() => createSetup({name: 'name'})}</div>),
document.getElementById('root'););
Running this does not show the HOC, only the div
Can anyone help out with a better example than the ones given?
A HOC is a function that takes a Component as one of its parameters and enhances that component in some way.
If HOCs create a new enhanced component, can it be possible not to pass in any component as argument at all?
Nope, then it wouldn't be a HOC, because one of the conditions is that they take a component as one of the arguments and they return a new Component that has some added functionality.
In an example such as this, which is the higher order component, the Button or the EnhancedButton.
EnhanceButton is the HOC and FinalButton is the enhanced component.
I tried creating one HOC like this: ... Running this does not show the HOC, only the div
That's because your createSetup function is not a HOC... It's a function that returns a component, yes, but it does not take a component as an argument in order to enhance it.
Let's see an example of a basic HOC:
const renderWhen = (condition, Component) =>
props => condition(props)
? <Component {...props} />
: null
);
And you could use it like this:
const EnhancedLink = renderWhen(({invisible}) => !invisible, 'a');
Now your EnhancedLink will be like a a component but if you pass the property invisible set to true it won't render... So we have enhanced the default behaviour of the a component and you could do that with any other component.
In many cases HOC functions are curried and the Component arg goes last... Like this:
const renderWhen = condition => Component =>
props => condition(props)
? <Component {...props} />
: null
);
Like the connect function of react-redux... That makes composition easier. Have a look at recompose.
In short, If you assume functions are analogues to Components, Closure is analogous to HOC.
Try your createSetup.js with:
const createSetup = options => <p>{options.name}</p>;
and your main.js
const comp = createSetup({ name: 'name' });
render((<div>{comp}</div>),
document.getElementById('root'));
A higher-order component (HOC) is an advanced technique in React for reusing component logic. Concretely, a higher-order component is a function that takes a component and returns a new component.
A HOC is a pure function with zero side-effects.
Example: CONDITIONALLY RENDER COMPONENTS
Suppose we have a component that needs to be rendered only when a user is authenticated — it is a protected component. We can create a HOC named WithAuth() to wrap that protected component, and then do a check in the HOC that will render only that particular component if the user has been authenticated.
A basic withAuth() HOC, according to the example above, can be written as follows:
// withAuth.js
import React from "react";
export function withAuth(Component) {
return class AuthenticatedComponent extends React.Component {
isAuthenticated() {
return this.props.isAuthenticated;
}
/**
* Render
*/
render() {
const loginErrorMessage = (
<div>
Please login in order to view this part of the application.
</div>
);
return (
<div>
{ this.isAuthenticated === true ? <Component {...this.props} /> : loginErrorMessage }
</div>
);
}
};
}
export default withAuth;
The code above is a HOC named withAuth. It basically takes a component and returns a new component, named AuthenticatedComponent, that checks whether the user is authenticated. If the user is not authenticated, it returns the loginErrorMessage component; if the user is authenticated, it returns the wrapped component.
Note: this.props.isAuthenticated has to be set from your application’s
logic. (Or else use react-redux to retrieve it from the global state.)
To make use of our HOC in a protected component, we’d use it like so:
// MyProtectedComponent.js
import React from "react";
import {withAuth} from "./withAuth.js";
export class MyProectedComponent extends React.Component {
/**
* Render
*/
render() {
return (
<div>
This is only viewable by authenticated users.
</div>
);
}
}
// Now wrap MyPrivateComponent with the requireAuthentication function
export default withAuth(MyPrivateComponent);
Here, we create a component that is viewable only by users who are authenticated. We wrap that component in our withAuth HOC to protect the component from users who are not authenticated.
Source
// HIGHER ORDER COMPOENTS IN REACT
// Higher order components are JavaScript functions used for adding
// additional functionalities to the existing component.
// file 1: hoc.js (will write our higher order component logic) -- code start -->
const messageCheckHOC = (OriginalComponent) => {
// OriginalComponent is component passed to HOC
const NewComponent = (props) => {
// business logic of HOC
if (!props.isAllowedToView) {
return <b> Not Allowed To View The MSG </b>;
}
// here we can pass the props to component
return <OriginalComponent {...props} />;
};
// returning new Component with updated Props and UI
return NewComponent;
};
export default messageCheckHOC;
// file 1: hoc.js -- code end -->
// file 2: message.js -- code start -->
// this is the basic component we are wrapping with HOC
// to check the permission isAllowedToView msg if not display fallback UI
import messageCheckHOC from "./hoc";
const MSG = ({ name, msg }) => {
return (
<h3>
{name} - {msg}
</h3>
);
};
export default messageCheckHOC(MSG);
// file 2: message.js -- code end -->
// file 3 : App.js -- code start --->
import MSG from "./message.js";
export default function App() {
return (
<div className="App">
<h3>HOC COMPONENTS </h3>
<MSG name="Mac" msg="Heyy !!! " isAllowedToView={true} />
<MSG name="Robin" msg="Hello ! " isAllowedToView={true} />
<MSG name="Eyann" msg="How are you" isAllowedToView={false} />
</div>
);
}
// file 3 : App.js -- code end --->

Categories

Resources