Movatterモバイル変換


[0]ホーム

URL:


We want to hear from you!Take our 2021 Community Survey!
This site is no longer updated.Go to react.dev

Higher-Order Components

These docs are old and won’t be updated. Go toreact.dev for the new React docs.

Higher-order components are not commonly used in modern React code.

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

Concretely,a higher-order component is a function that takes a component and returns a new component.

const EnhancedComponent=higherOrderComponent(WrappedComponent);

Whereas a component transforms props into UI, a higher-order component transforms a component into another component.

HOCs are common in third-party React libraries, such as Redux’sconnect and Relay’screateFragmentContainer.

In this document, we’ll discuss why higher-order components are useful, and how to write your own.

Use HOCs For Cross-Cutting Concerns

Note

We previously recommended mixins as a way to handle cross-cutting concerns. We’ve since realized that mixins create more trouble than they are worth.Read more about why we’ve moved away from mixins and how you can transition your existing components.

Components are the primary unit of code reuse in React. However, you’ll find that some patterns aren’t a straightforward fit for traditional components.

For example, say you have aCommentList component that subscribes to an external data source to render a list of comments:

classCommentListextendsReact.Component{constructor(props){super(props);this.handleChange=this.handleChange.bind(this);this.state={// "DataSource" is some global data sourcecomments: DataSource.getComments()};}componentDidMount(){// Subscribe to changes    DataSource.addChangeListener(this.handleChange);}componentWillUnmount(){// Clean up listener    DataSource.removeChangeListener(this.handleChange);}handleChange(){// Update component state whenever the data source changesthis.setState({comments: DataSource.getComments()});}render(){return(<div>{this.state.comments.map((comment)=>(<Commentcomment={comment}key={comment.id}/>))}</div>);}}

Later, you write a component for subscribing to a single blog post, which follows a similar pattern:

classBlogPostextendsReact.Component{constructor(props){super(props);this.handleChange=this.handleChange.bind(this);this.state={blogPost: DataSource.getBlogPost(props.id)};}componentDidMount(){    DataSource.addChangeListener(this.handleChange);}componentWillUnmount(){    DataSource.removeChangeListener(this.handleChange);}handleChange(){this.setState({blogPost: DataSource.getBlogPost(this.props.id)});}render(){return<TextBlocktext={this.state.blogPost}/>;}}

CommentList andBlogPost aren’t identical — they call different methods onDataSource, and they render different output. But much of their implementation is the same:

  • On mount, add a change listener toDataSource.
  • Inside the listener, callsetState whenever the data source changes.
  • On unmount, remove the change listener.

You can imagine that in a large app, this same pattern of subscribing toDataSource and callingsetState will occur over and over again. We want an abstraction that allows us to define this logic in a single place and share it across many components. This is where higher-order components excel.

We can write a function that creates components, likeCommentList andBlogPost, that subscribe toDataSource. The function will accept as one of its arguments a child component that receives the subscribed data as a prop. Let’s call the functionwithSubscription:

const CommentListWithSubscription=withSubscription(  CommentList,(DataSource)=> DataSource.getComments());const BlogPostWithSubscription=withSubscription(  BlogPost,(DataSource, props)=> DataSource.getBlogPost(props.id));

The first parameter is the wrapped component. The second parameter retrieves the data we’re interested in, given aDataSource and the current props.

WhenCommentListWithSubscription andBlogPostWithSubscription are rendered,CommentList andBlogPost will be passed adata prop with the most current data retrieved fromDataSource:

// This function takes a component...functionwithSubscription(WrappedComponent, selectData){// ...and returns another component...returnclassextends React.Component{constructor(props){super(props);this.handleChange=this.handleChange.bind(this);this.state={data:selectData(DataSource, props)};}componentDidMount(){// ... that takes care of the subscription...      DataSource.addChangeListener(this.handleChange);}componentWillUnmount(){      DataSource.removeChangeListener(this.handleChange);}handleChange(){this.setState({data:selectData(DataSource,this.props)});}render(){// ... and renders the wrapped component with the fresh data!// Notice that we pass through any additional propsreturn<WrappedComponentdata={this.state.data}{...this.props}/>;}};}

Note that a HOC doesn’t modify the input component, nor does it use inheritance to copy its behavior. Rather, a HOCcomposes the original component bywrapping it in a container component. A HOC is a pure function 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.

BecausewithSubscription is a normal function, you can add as many or as few arguments as you like. For example, you may want to make the name of thedata prop configurable, to further isolate the HOC from the wrapped component. Or you could accept an argument that configuresshouldComponentUpdate, 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 betweenwithSubscription 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.

functionlogProps(InputComponent){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 receivedconst EnhancedComponent=logProps(InputComponent);

There are a few problems with this. One is that the input component cannot be reused separately from the enhanced component. More crucially, if you apply another HOC toEnhancedComponent thatalso mutatescomponentDidUpdate, the first HOC’s functionality will be overridden! This HOC also won’t work with function components, which do not have lifecycle methods.

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:

functionlogProps(WrappedComponent){returnclassextends 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. And because it’s a pure function, it’s composable with other HOCs, or even with itself.

You may have noticed similarities between HOCs and a pattern calledcontainer components. 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.

Convention: Pass Unrelated Props Through to the Wrapped Component

HOCs add features to a component. They shouldn’t drastically alter its contract. It’s expected that the component returned from a HOC has a similar interface to the wrapped component.

HOCs should pass through props that are unrelated to its specific concern. Most HOCs contain a render method that looks something like this:

render(){// Filter out extra props that are specific to this HOC and shouldn't be// passed throughconst{ extraProp,...passThroughProps}=this.props;// Inject props into the wrapped component. These are usually state values or// instance methods.const injectedProp= someStateOrInstanceMethod;// Pass props to wrapped componentreturn(<WrappedComponentinjectedProp={injectedProp}{...passThroughProps}/>);}

This convention helps ensure that HOCs are as flexible and reusable as possible.

Convention: Maximizing Composability

Not all HOCs look the same. Sometimes they accept only a single argument, the wrapped component:

const NavbarWithRouter=withRouter(Navbar);

Usually, HOCs accept additional arguments. In this example from Relay, a config object is used to specify a component’s data dependencies:

const CommentWithRelay= Relay.createContainer(Comment, config);

The most common signature for HOCs looks like this:

// React Redux's `connect`const ConnectedComment=connect(commentSelector, commentActions)(CommentList);

What?! If you break it apart, it’s easier to see what’s going on.

// connect is a function that returns another functionconst enhance=connect(commentListSelector, commentListActions);// The returned function is a HOC, which returns a component that is connected// to the Redux storeconst ConnectedComment=enhance(CommentList);

In other words,connect is a higher-order function that returns a higher-order component!

This form may seem confusing or unnecessary, but it has a useful property. Single-argument HOCs like the one returned by theconnect function have the signatureComponent => Component. Functions whose output type is the same as its input type are really easy to compose together.

// Instead of doing this...const EnhancedComponent=withRouter(connect(commentSelector)(WrappedComponent))// ... you can use a function composition utility// compose(f, g, h) is the same as (...args) => f(g(h(...args)))const enhance=compose(// These are both single-argument HOCs  withRouter,connect(commentSelector))const EnhancedComponent=enhance(WrappedComponent)

(This same property also allowsconnect and other enhancer-style HOCs to be used as decorators, an experimental JavaScript proposal.)

Thecompose utility function is provided by many third-party libraries including lodash (aslodash.flowRight),Redux, andRamda.

Convention: Wrap the Display Name for Easy Debugging

The container components created by HOCs show up in theReact Developer Tools like any other component. To ease debugging, choose a display name that communicates that it’s the result of a HOC.

The most common technique is to wrap the display name of the wrapped component. So if your higher-order component is namedwithSubscription, and the wrapped component’s display name isCommentList, use the display nameWithSubscription(CommentList):

functionwithSubscription(WrappedComponent){classWithSubscriptionextendsReact.Component{/* ... */}  WithSubscription.displayName=`WithSubscription(${getDisplayName(WrappedComponent)})`;return WithSubscription;}functiongetDisplayName(WrappedComponent){return WrappedComponent.displayName|| WrappedComponent.name||'Component';}

Caveats

Higher-order components come with a few caveats that aren’t immediately obvious if you’re new to React.

Don’t Use HOCs Inside the render Method

React’s diffing algorithm (calledReconciliation) uses component identity to determine whether it should update the existing subtree or throw it away and mount a new one. If the component returned fromrender is identical (===) to the component from the previous render, React recursively updates the subtree by diffing it with the new one. If they’re not equal, the previous subtree is unmounted completely.

Normally, you shouldn’t need to think about this. But it matters for HOCs because it means you can’t apply a HOC to a component within the render method of a component:

render(){// A new version of EnhancedComponent is created on every render// EnhancedComponent1 !== EnhancedComponent2const EnhancedComponent=enhance(MyComponent);// That causes the entire subtree to unmount/remount each time!return<EnhancedComponent/>;}

The problem here isn’t just about performance — remounting a component causes the state of that component and all of its children to be lost.

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 you want, anyway.

In those rare cases where you need to apply a HOC dynamically, you can also do it inside a component’s lifecycle methods or its constructor.

Static Methods Must Be Copied Over

Sometimes it’s useful to define a static method on a React component. For example, Relay containers expose a static methodgetFragment to facilitate the composition of GraphQL fragments.

When you apply a HOC to a component, though, the original component is wrapped with a container component. That means the new component does not have any of the static methods of the original component.

// Define a static methodWrappedComponent.staticMethod=function(){/*...*/}// Now apply a HOCconst EnhancedComponent=enhance(WrappedComponent);// The enhanced component has no static methodtypeof EnhancedComponent.staticMethod==='undefined'// true

To solve this, you could copy the methods onto the container before returning it:

functionenhance(WrappedComponent){classEnhanceextendsReact.Component{/*...*/}// Must know exactly which method(s) to copy :(  Enhance.staticMethod= WrappedComponent.staticMethod;return Enhance;}

However, this requires you to know exactly which methods need to be copied. You can usehoist-non-react-statics to automatically copy all non-React static methods:

import hoistNonReactStaticfrom'hoist-non-react-statics';functionenhance(WrappedComponent){classEnhanceextendsReact.Component{/*...*/}hoistNonReactStatic(Enhance, WrappedComponent);return Enhance;}

Another possible solution is to export the static method separately from the component itself.

// Instead of...MyComponent.someFunction= someFunction;exportdefault MyComponent;// ...export the method separately...export{ someFunction};// ...and in the consuming module, import bothimport MyComponent,{ someFunction}from'./MyComponent.js';

Refs Aren’t Passed Through

While the convention for higher-order components is to pass through all props to the wrapped component, this does not work for refs. That’s becauseref is not really a prop — likekey, it’s handled specially by React. If you add a ref to an element whose component is the result of a HOC, the ref refers to an instance of the outermost container component, not the wrapped component.

The solution for this problem is to use theReact.forwardRef API (introduced with React 16.3).Learn more about it in the forwarding refs section.

Is this page useful?Edit this page

[8]ページ先頭

©2009-2025 Movatter.jp