神刀安全网

React Higher Order Components in depth

Abstract

This post is targeted to advanced users that want to exploit the HOC pattern. If you are new to React you should probably start by reading React’s Docs .

Higher Order Components is a great Pattern that has proven to be very valuable for several React libraries. In this Post we will review in detail what HOCs are, what you can do with them and their limitations and, how they are implemented.

In the Appendixes we review related topics that are not core to HOC study but I thought we should have covered.

This Post is meant to be exhaustive so if you find anything I have missed please report it and I will make the required changes.

This Post assumes ES6 knowledge.

Let’s dive in!

What are Higher Order Components?

A Higher Order Component is just a React Component that wraps another one.

This pattern is usually implemented as a function, which is basically a class factory (yes, a class factory!), that has the following signature in haskell inspired pseudocode

hocFactory:: W: React.Component => E: React.Component

Where W ( WrappedComponent ) is the React.Component being wrapped and E (Enhanced Component) is the new, HOC, React.Component being returned.

The “wraps” part of the definition is intentionally vague because it can mean one of two things:

  1. Props Proxy: The HOC manipulates the props being passed to the WrappedComponent W ,
  2. Inheritance Inversion: The HOC extends the WrappedComponent W .

We will explore this two patterns in more detail.

What can I do with HOCs?

At a high level HOC enables you to:

  • Code reuse, logic and bootstrap abstraction
  • Render Highjacking
  • State abstraction and manipulation
  • Props manipulation

We will see this items in more detail soon but first, we are going to study the ways of implementing HOCs because the implementation allows and restricts what you can actually do with an HOC.

HOC factory implementations

In this section we are going to study the two main ways of implementing HOCs in React: Props Proxy (PP) and Inheritance Inversion (II). Both enable different ways of manipulating the WrappedComponent .

Props Proxy

Props Proxy (PP) is implemented trivially in the following way:

The important part here is that the render method of the HOC returns a React Element of the type of the WrappedComponent . We also pass through the props that the HOC receives, hence the name Props Proxy .

NOTE:

<WrappedComponent {...this.props}/>
// is equivalent to
React.createElement(WrappedComponent, this.props, null)

They both create a React Element that describes what React should render in its reconciliation process, if you want to know more about React Elements vs Components see this post by Dan Abramov and see the docs to read more about the reconciliation process .

What can be done with Props Proxy?

  • Manipulating props
  • Accessing the instance via Refs
  • Abstracting State
  • Wrapping the WrappedComponent with other elements

Manipulating props

You can read, add, edit and remove the props that are being passed to the WrappedComponent .

Be careful with deleting or editing important props, you should probably namespace your Higher Order props not to break the WrappedComponent .

Example: Adding new props. The app’s current logged in user will be available in the WrappedComponent via this.props.user

Accessing the instance via Refs

You can access this (the instance of the WrappedComponent ) with a ref , but you will need a full initial normal render process of the WrappedComponent for the ref to be calculated, that means that you need to return the WrappedComponent element from the HOC render method, let React do it’s reconciliation process and just then you will have a ref to the WrappedComponent instance.

Example: In the following example we explore how to access instance methods and the instance itself of the WrappedComponent via refs

When the WrappedComponent is rendered the ref callback will be executed, and then you will have a reference to the WrappedComponent ’s instance. This can be used for reading/adding instance props and to call instance methods.

State abstraction

You can abstract state by providing props and callbacks to the WrappedComponent , very similar to how smart components will deal with dumb components. See dumb and smart components for more information.

Example: In the following State Abstraction example we naively abstract the value and onChange handler of the name input field. I say naively because this isn’t very general but you got to see the point.

You would use it like this:

The input will be a controlled input automagically.

For a more general two way data bindings HOC see this link

Wrapping the WrappedComponent with other elements

You can wrap the WrappedComponent with other components and elements for styling, layout or other purposes. Some basic usages can be accomplished by regular Parent Components (see Appendix B) but you have more flexibility with HOCs as described previously.

Example: Wrapping for styling purposes

Inheritance Inversion

Inheritance Inversion (II) is implemented trivially like this:

As you can see, the returned HOC class (Enhancer) extends the WrappedComponent. It is called Inheritance Inversion because instead of the WrappedComponent extending some Enhancer class, it is passively extended by the Enhancer . In this way the relationship between them seems inverse .

Inheritance Inversion allows the HOC to have access to the WrappedComponent instance via this , which means it has access to the state, props, component lifecycle hooks and the render method .

I won’t go into much detail about what you can do with lifecycle hooks because it’s not HOC specific but React specific. But note that with II you can create new lifecycle hooks for the WrappedComponent. Remember to always call super.[lifecycleHook] so you don’t break the WrappedComponent

Reconciliation process

Before diving in let’s summarize some theory.

React Elements describe what is going to be rendered when React runs it’s reconciliation process.

React Elements can be of two types: String and Function. The String Type React Elements (STRE) represent DOM nodes and the Function Type React Elements (FTRE) represent Components created by extending React.Component. For more about Elements and Components read this post .

FTRE will be resolved to a full STRE tree in React’s reconciliation process (the end result will be always DOM elements).

This is very important and it means that Inheritance Inversion High Order Components don’t have a guaranty of having the full children tree resolved .

Inheritance Inversion High Order Components don’t have a guaranty of having the full children tree resolved.

This is will prove important when studying Render Highjacking.

What can you do with Inheritance Inversion?

  • Render Highjacking
  • Manipulating state

Render Highjacking

It is called Render Highjacking because the HOC takes control of the render output of the WrappedComponent and can do all sorts of stuff with it.

In Render Highjacking you can:

  • Read, add, edit, remove props in any of the React Elements outputted by render
  • Read, and modify the React Elements tree outputted by render
  • Conditionally display the elements tree
  • Wrapping the element’s tree for styling purposes (as shown in Props Proxy)

*render refers to the WrappedComponent . render method

You cannot edit or create props of the W rappedComponent instance, because a React Component cannot edit the props it receives, but you can change the props of the elements that are outputted from the render method.

Just as we studied before, II HOCs don’t have a guaranty of having the full children tree resolved, which implies some limits to the Render Highjacking technique. The rule of thumb is that with Render Highjacking you will be able to manipulate the element’s tree that the WrappedComponent render method outputs no more no less. If that element’s tree includes a Function Type React Component then you won’t be able to manipulate that Component’s children. (They are deferred by React’s reconciliation process until it actually renders to the screen.)

Example 1: Conditional rendering. This HOC will render exactly what the WrappedComponent would render unless this.props.loggedIn is not true. (Assuming the HOC will receive the loggedIn prop)

Example 2: Modifying the React Elements tree outputted by render .

In this example, if the rendered output of the WrappedComponent has an input as it’s top level element then we change the value to “may the force be with you” .

You can do all sorts of stuff in here, you can traverse the entire elements tree and change props of any element in the tree. This is exactly how Radium does its business (More on Radium in Case Studies).

NOTE: You cannot Render Highjack with Props Proxy.

While it is possible to access the render method via WrappedComponent.prototype.render, you will need to mock the WrappedComponent instance and its props, and potentially handle the component lifecycle yourself, instead of relying on React doing it. In my experiments it isn’t worth it and if you want to do Render Highjacking you should be using Inheritance Inversion instead of Props Proxy. Remember that React handles component instances internally and your only way of dealing with instances is via this or by refs.

Manipulating state

The HOC can read, edit and delete state of the WrappedComponent instance, and you can also add more state if you need to. Remember that you are messing with the state of the WrappedComponent which can lead to you breaking things. Mostly the HOC should be limited to read or add state, and the latter should be namespaced not to mess with the WrappedComponent’s state.

Example: Debugging by accessing WrappedComponent ’s props and state

This HOC wrapps the WrappedComponent with other elements, and also displays the WrappedComponent ’s instance props and state. The JSON.stringify trick was taught to me by Ryan Florence and Michael Jackson . You can see a full working implementation of the debugger here .

Naming

When wrapping a component with an HOC you lose the original WrappedComponent ’s name which might impact you when developing and debugging.

What people usually do is to customize the HOC’s name by taking the WrappedComponent ’s name and prepending something. The following is taken from React-Redux.

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}

The getDisplayName function is defined as follows:

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}

You actually don’t need to rewrite it yourself because recompose lib already provides this function.

Case Studies

React-Redux

React-Redux is the official Redux bindings for React. One of the functions it provides is connect which handles all the bootstrap necessary for listening to the store and cleaning up afterwards. This is achieved by a Props Proxy implementation.

If you have ever worked with pure Flux you know that any React Component that is connected to one or more stores needs a lot of bootstrapping for adding and removing stores listeners and selecting the parts of the state they need. So React-Redux implementation is pretty good because it abstract all this bootstrap. Basically, you don’t need to write it yourself anymore!

Radium

Radium is a library that enhances the capability of inline styles by enabling CSS pseudo selectors inside inline-styles. Why inline styles are good for you is subject of another discussion, but a lot of people are starting to do it and libs like radium really step up the game. If you want to know more about inline styles start by this p resentation byVjeux

So, how does Radium enables inline CSS pseudo selectors like hover? It implements an Inheritance Inversion pattern to use Render Highjacking in order to inject proper event listeners (new props) to simulate CSS pseudo selectors like hover. The event listeners are injected as handlers for React Element’s props. This requires Radium to read all the Elements tree outputted by the WrappedComponent ’s render method and whenever it finds an element with a style prop, it adds event listeners props. Simply put, Radium modifies the props of the Element’s tree (what Radium actually does is a little bit more complicated but you get the point)

Radium exposes a really simple API. Pretty impressive considering all the work it performs without the user even noticing. This gives a glimpse of the power of HOC.

Appendix A: HOC and parameters

The following content is optional and you may skip it.

Sometimes is useful to use parameters on your HOCs. This is implicit in all examples above and should be pretty natural to intermediate Javascript developers, but just for the sake of making the post exhaustive let’s cover it real quick.

Example: HOC parameters with a trivial Props Proxy. The important thing is the HOCFactoryFactory function.

You can use it like this:

HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}

Appendix B: Difference with Parent Components

The following content is optional and you may skip it.

Parent Components are just React Components that have some children. React has APIs for accessing and manipulating a component’s children.

Example: Parent Components accessing its children.

Now we will review what Parent Components can and cannot do in contrast of HOCs plus some important details:

  • Render Highjacking (as seen in Inheritance Inversion)
  • Manipulate inner props (as seen in Inheritance Inversion)
  • Abstract state. But has its drawbacks. You wont be able to access the state of the Parent Component from outside it unless you explicitly create hooks for it. This makes its usefulness restricted.
  • Wrapp with new React Elements. This might be the single use case where Parent Components feel more ergonomic than HOC. HOCs can do this too.
  • Children manipulation has some gotchas. For example if children don’t got a single root element (more than one first level children), then you need to add an extra element to wrap all children, which might be a little bit cumbersome to your markup. In HOCs a single top level children root is guarantied by React/JSX constraints.
  • Parent Components can be used freely in an Elements tree, they are not restricted to once per Component class as HOCs are.

Generally, if you can do it with Parent Components you should because it’s much less hacky than HOCs, but as the list above states they are less flexible than HOCs.

Closing Words

I hope that after reading this post you know a little more about React HOCs. They are highly expressive and have proven to be pretty good in different libraries.

React has brought a lot of innovation and people running projects like Radium, React-Redux, React-Router, among others, are pretty good proofs of that.

If want to contact me please follow me on twitter @franleplant.

Go to this repo to play with code I’ve played with in order to experiment some of the patterns explained in this post.

Credits

Credits go mostly to React-Redux, Radium, this gist by Sebastian Markbåge and my own experimentation.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » React Higher Order Components in depth

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址