|
| 1 | +### Higher Order Components |
| 2 | + |
| 3 | +Nitish Kumar |
| 4 | +[@nitishk88](https://twitter.com/nitishk88) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +### Introduction |
| 9 | + |
| 10 | +A higher-order component(HOC) is a technique in React for reusing component logic. |
| 11 | +A HOC is a pure function that takes a compoenent and returns a new component. |
| 12 | + |
| 13 | +```js |
| 14 | +const EnhancedComponent = higherOrderComponent(WrappedComponent); |
| 15 | +``` |
| 16 | + |
| 17 | +--- |
| 18 | + |
| 19 | +### Concept and Usage |
| 20 | + |
| 21 | +HOCs should be used for cross-cutting concerns. |
| 22 | + |
| 23 | +* Patterns that repeat can be identified, abstracted and shared across multiple components. |
| 24 | +* A function can be written that takes a component, applies the re-usable logic and returns a new |
| 25 | + component with original and additional calculated props. |
| 26 | +* HOC neither modifies the input component, nor does it use inheritance to copy its behavior. Rather, |
| 27 | + it composes the original component by wrapping it in a container component. |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +### Convention - Pass unrelated props through to the Wrapped Component |
| 32 | + |
| 33 | +To avoid altering the contract of a component, HOCs should pass through props that are |
| 34 | +unrelated to its specific concern. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +```js |
| 39 | +render() { |
| 40 | + // Filter out extra props that are specific to this HOC and shouldn't be |
| 41 | + // passed through |
| 42 | + const { extraProp, ...passThroughProps } = this.props; |
| 43 | + |
| 44 | + // Inject props into the wrapped component. These are usually state values or |
| 45 | + // instance methods. |
| 46 | + const injectedProp = someStateOrInstanceMethod; |
| 47 | + |
| 48 | + // Pass props to wrapped component |
| 49 | + return ( |
| 50 | + <WrappedComponent |
| 51 | + injectedProp={injectedProp} |
| 52 | + {...passThroughProps} |
| 53 | + /> |
| 54 | + ); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +### Convention - Design HOC for maximum composability |
| 61 | + |
| 62 | +HOCs can be designed to accept multiple arguments with the wrapped component. |
| 63 | + |
| 64 | +```js |
| 65 | +const NavbarWithRouter = withRouter(Navbar); |
| 66 | +const CommentWithRelay = Relay.createContainer(Comment, config); |
| 67 | +``` |
| 68 | + |
| 69 | +Single-argument HOCs have the signature `Component => Component`. These kind of functions are really |
| 70 | +easy to compose together. |
| 71 | + |
| 72 | +```js |
| 73 | +compose(f, g, h); // same as (...args) => f(g(h(...args))) |
| 74 | +``` |
| 75 | +--- |
| 76 | + |
| 77 | +In case where a HOC requires argument(s) other than the wrapped component, a higher-order function that returns a HOC can be utilized. |
| 78 | + |
| 79 | +```js |
| 80 | +// connect is a function that returns another function |
| 81 | +const enhance = connect(commentListSelector, commentListActions); |
| 82 | +// The returned function is a HOC, which returns a component that is connected |
| 83 | +// to the Redux store |
| 84 | +const ConnectedComment = enhance(CommentList); |
| 85 | +``` |
| 86 | + |
| 87 | +Function composition becomes feasible again in this case. |
| 88 | + |
| 89 | +```js |
| 90 | +const enhance = compose( |
| 91 | + // These are both single-argument HOCs |
| 92 | + withRouter, |
| 93 | + connect(commentSelector) |
| 94 | +); |
| 95 | +const EnhancedComponent = enhance(WrappedComponent); |
| 96 | +``` |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +### Convention - Wrap the Display Name for Easy Debugging |
| 101 | + |
| 102 | +Containers created by HOCs show up in the React Developer Tools like any other component. |
| 103 | +A display name that depicts the usage of HOC can be used to ease debugging. |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +```js |
| 108 | +const withSubscription = WrappedComponent => { |
| 109 | + class WithSubscription extends React.Component { |
| 110 | + /* ... */ |
| 111 | + } |
| 112 | + WithSubscription.displayName = `WithSubscription(${getDisplayName( |
| 113 | + WrappedComponent |
| 114 | + )})`; |
| 115 | + return WithSubscription; |
| 116 | +}; |
| 117 | + |
| 118 | +const getDisplayName = WrappedComponent => { |
| 119 | + return WrappedComponent.displayName || WrappedComponent.name || "Component"; |
| 120 | +}; |
| 121 | +``` |
| 122 | + |
| 123 | +--- |
| 124 | + |
| 125 | +### Caveats - Applying HOCs Inside the render Method |
| 126 | + |
| 127 | +```js |
| 128 | +render() { |
| 129 | + // A new version of EnhancedComponent is created on every render |
| 130 | + // EnhancedComponent1 !== EnhancedComponent2 |
| 131 | + const EnhancedComponent = enhance(MyComponent); |
| 132 | + // That causes the entire subtree to unmount/remount each time! |
| 133 | + return <EnhancedComponent />; |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +* Instead, apply HOCs outside the component definition so that the resulting component is created only once. |
| 140 | + Its identity will be consistent across renders in that case. |
| 141 | + |
| 142 | +* In cases where you need to apply a HOC dynamically, you can also do it inside a component’s lifecycle methods or its constructor. |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +### Caveats - Static Methods Must be Copied Over |
| 147 | + |
| 148 | +```js |
| 149 | +const enhance = WrappedComponent => { |
| 150 | + class Enhance extends React.Component { |
| 151 | + /*...*/ |
| 152 | + } |
| 153 | + // Must know exactly which method(s) to copy :( |
| 154 | + Enhance.staticMethod = WrappedComponent.staticMethod; |
| 155 | + return Enhance; |
| 156 | +}; |
| 157 | +``` |
| 158 | +--- |
| 159 | + |
| 160 | +Use `hoist-non-react-statics` to automatically copy all non-React static methods: |
| 161 | + |
| 162 | +```js |
| 163 | +import hoistNonReactStatic from "hoist-non-react-statics"; |
| 164 | +const enhance = WrappedComponent => { |
| 165 | + class Enhance extends React.Component { |
| 166 | + /*...*/ |
| 167 | + } |
| 168 | + hoistNonReactStatic(Enhance, WrappedComponent); |
| 169 | + return Enhance; |
| 170 | +}; |
| 171 | +``` |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## Exercise |
| 176 | + |
| 177 | +* Create a HOC that logs the Wrapped Component's old props and new props to console. |
| 178 | +* It should be displayed as ```LogProps``` in the React Dev Tools. |
0 commit comments