ref – https://reactjs.org/docs/higher-order-components.html
https://www.codingame.com/playgrounds/8595/reactjs-higher-order-components-tutorial
Whereas a component transforms props into UI, a higher-order component transforms a component into another component.
A HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOC composes the original component by wrapping it in a container component. A HOC is a pure function (does not modify outside values, unique input/output) with zero side-effects.
And that’s it! The wrapped component receives all the props of the container, along with a new prop, data, which it uses to render its output. The HOC isn’t concerned with how or why the data is used, and the wrapped component isn’t concerned with where the data came from.
Let’s look at an example. Create a simple React app like so.
npm install -g create-react-app
create-react-app my-app
cd my-app
npm start
It’s only a Higher Order component by definition because it does not modify outside values. It simply take another component as input and renders it.
Let’s create a generic utility component that displays data in a row. It will be used by two other components.
TableRow.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { Component } from 'react'; class TableRow extends Component { render() { return ( <tr> <td> {this.props.obj.id} </td> <td> {this.props.obj.name} </td> </tr> ); } } export default TableRow; |
StockList.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import React, { Component } from 'react'; import TableRow from './TableRow'; class StockList extends Component { // now our prop will have the data we need // we use our static data and display it using a very simple TableRow component tabRow() { if(this.props.data instanceof Array){ return this.props.data.map(function(object, i){ return <TableRow obj={object} key={i} />; }) } } render() { return ( <div className="container"> <table className="table table-striped"> <thead> <tr> <td>Stock Name</td> <td>Stock Price</td> </tr> </thead> <tbody> {this.tabRow()} </tbody> </table> </div> ); } } export default StockList; |
Now we use that StockList in our App.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// App.js import React, { Component } from 'react'; import StockList from './StockList'; class App extends Component { render() { return ( <div> <StockList></StockList> </div> ) } } export default App; |
Everything is standard. We display a component inside of another component. But let’s say we want to add UserList, which is very very similar to StockList.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import React, { Component } from 'react'; import TableRow from './TableRow'; class UserList extends Component { constructor(props) { super(props); this.state = { users: [ { id: 1, name: 'Krunal' }, { id: 2, name: 'Ankit' }, { id: 3, name: 'Rushabh' } ] }; } tabRow(){ if(this.state.users instanceof Array){ return this.state.users.map(function(object, i){ return <TableRow obj={object} key={i} />; }) } } render() { return ( <div className="container"> <table className="table table-striped"> <thead> <tr> <td>ID</td> <td>Name</td> </tr> </thead> <tbody> {this.tabRow()} </tbody> </table> </div> ); } } export default UserList; |
It even uses the TableRow component in the same way. Can we somehow refactor these two components into one?
This is where HOC comes into play. We can implement reusability of particular components in multiple modules or components.
Take a look at how we’d factor this. Let’s create an HOC file.
HOC.js
In our HOC, we receive the two components. However, we combine the functionality of passing data to them. Instead of them having its own data state.
Hence, we receive data from the parameter and use prop to pass it into those Wrapped components.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import React, {Component} from 'react'; // let's edit our Hoc function so that it caters to both StockList and UserList export default function Hoc( HocComponent, data) { return class extends Component { constructor(props) { super(props); this.state = {data} // assign comp state to passed in data } render() { // we return the passed in component, but we pass our state into it // via prop 'data', those two components (Stock and User) can access this data // of course, we also pass the rest of our props into it for configuration return ( <HocComponent data = {this.state.data} {...this.props} /> ); } } } |
In our redundant components, our this.prop will have the data we need, instead of us keeping the data state in our component.
Thus, we use it right away in our render functions.
StockList.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// StockList.js import React, { Component } from 'react'; import TableRow from './TableRow'; class StockList extends Component { // now our prop will have the data we need // we use our static data and display it using a very simple TableRow component tabRow() { if(this.props.data instanceof Array){ return this.props.data.map(function(object, i){ return <TableRow obj={object} key={i} />; }) } } render() { return ( <div className="container"> <table className="table table-striped"> <thead> <tr> <td>Stock Name</td> <td>Stock Price</td> </tr> </thead> <tbody> {this.tabRow()} </tbody> </table> </div> ); } } export default StockList; |
In both UserList and StockList, we are doing the same thing.
Just display the stocks and users properties id and name.
So here our component gets passed into our HOC, which then fills our
this.prop with the new data.
We then can display it.
UserList.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import React, { Component } from 'react'; import TableRow from './TableRow'; class UserList extends Component { tabRow(){ if(this.props.data instanceof Array){ return this.props.data.map(function(object, i){ return <TableRow obj={object} key={i} />; }) } } render() { return ( <div className="container"> <table className="table table-striped"> <thead> <tr> <td>ID</td> <td>Name</td> </tr> </thead> <tbody> {this.tabRow()} </tbody> </table> </div> ); } } export default UserList; |
We pull the data state out. Then pass both data and components into HOC, which creates two different components.
App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import React from 'react'; import './App.css'; import Hoc from './HOC'; import StockList from './StockList'; import UserList from './UserList'; // so we pull the data out here. And then use it in our HOC const StocksData = [ { id: 1, name: 'TCS' }, { id: 2, name: 'Infosys' }, { id: 3, name: 'Reliance' } ]; const UsersData = [ { id: 1, name: 'Krunal' }, { id: 2, name: 'Ankit' }, { id: 3, name: 'Rushabh' } ]; const Stocks = Hoc( StockList, StocksData ); const Users = Hoc( UserList, UsersData ); // functional component - for display purposes // data configuration and usage separated to elsewhere function App() { return ( <div className="App"> <Stocks></Stocks> <Users></Users> </div> ); } export default App; |
Because Hoc is a normal function, you can add as many or as few arguments as you like. For example, you could accept an argument that configures shouldComponentUpdate, or one that configures the data source. These are all possible because the HOC has full control over how the component is defined.
Like components, the contract between Hoc and the wrapped component is entirely props-based. This makes it easy to swap one HOC for a different one, as long as they provide the same props to the wrapped component. This may be useful if you change data-fetching libraries, for example.
Don’t Mutate the Original Component. Use Composition.
Resist the temptation to modify a component’s prototype (or otherwise mutate it) inside a HOC.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function HOC(InputComponent) { // NO! InputComponent.prototype.componentDidUpdate = function(prevProps) { console.log('Current props: ', this.props); console.log('Previous props: ', prevProps); }; // The fact that we're returning the original input is a hint that it has // been mutated. return InputComponent; } // EnhancedComponent will log whenever props are received const EnhancedComponent = HOC(InputComponent); |
– The input component cannot be reused separately from the enhanced component because it depends on the original functionality of the input component. If you change it in your HOC, then your enhanced component will function differently from the original component.
– If you apply another HOC to EnhancedComponent that also mutates componentDidUpdate, the first HOC’s functionality will be overridden!
– Mutating HOCs are a leaky abstraction—the consumer must know how they are implemented in order to avoid conflicts with other HOCs.
Instead of mutation, HOCs should use composition, by wrapping the input component in a container component. In our example, we create a class component, then implement our componentDidUpdate the way we want, and then wrap our component inside the render via composition.
1 2 3 4 5 6 7 8 9 10 11 12 |
function HOC(WrappedComponent) { return class extends React.Component { componentDidUpdate(prevProps) { console.log('Current props: ', this.props); console.log('Previous props: ', prevProps); } render() { // Wraps the input component in a container, without mutating it. Good! return <WrappedComponent {...this.props} />; } } } |
This HOC has the same functionality as the mutating version while avoiding the potential for clashes.
– It works equally well with class and function components.
– Because it’s a pure function, it’s composable with other HOCs, or even with itself.
Container components are part of a strategy of separating responsibility between high-level and low-level concerns. Containers manage things like subscriptions and state, and pass props to components that handle things like rendering UI. HOCs use containers as part of their implementation. You can think of HOCs as parameterized container component definitions.
Don’t Use HOCs Inside the render Method
React’s diffing algorithm (called reconciliation) uses component identity to determine whether it should update the existing subtree or throw it away and mount a new one.
When its analyze the previous and current render, it looks at the previous and current component. If the two components are identical (===), React will leave the subtree be, and continue to update the subtree by diffing it with the new one. If they’re not equal, the previous subtree is unmounted completely.
This means you can’t apply a HOC to a component within the render method of a component because for every render, a new component (object) returned. Hence every compare will be different and thus, the entire subtree will be remount each time.
1 2 3 4 5 6 7 |
render() { // A new version of EnhancedComponent is created on every render // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // That causes the entire subtree to unmount/remount each time! return <EnhancedComponent />; } |
Remounting a component causes the state of that component and all of its children to be lost also.
Instead, apply HOCs outside the component definition so that the resulting component is created only once. Then, its identity will be consistent across renders. This is usually what how you use it.
In those very rare cases where you need to apply a HOC dynamically, you can also do it inside a component’s lifecycle methods or its constructor.