This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.
As your applications grow, you’ll want to reuse code where possible. Instead of writing a fetchUser
method on each component requiring the current user’s metadata, you’d write it once and then share it to the classes that need it. Ext JS made extensive use of mixins within the framework itself and provided a public mixin API for all Ext JS classes. In this article, we’ll look at how the same concept of shared code can be accomplished in React. To start, let’s first take a look at how to mix one class into another within Ext JS:
Ext JS Mixins
Ext.define('Foo', { mixins : [ 'Ext.mixin.Observable' ], constructor : function (config) { this.mixins.observable.constructor.call(this, config); } }); var instance = new Foo(); instance.fireEvent('foobar', instance);
Ext JS makes it easy to list the mixins you want on a class via the mixins
array. For the Observable
mixin, it also has a constructor
method, so we have to call it in our class’ constructor
. You can see when we create an instance of Foo
, we now have the fireEvent
method even though fireEvent
is not defined in the Foo
class. The method actually lives in the Observable
mixin which is now applied onto the Foo
class’ prototype.
HOCs to the rescue
Historically, React offered a React.createClass
method with a mixins
config option. The mixins
option accepted an array of objects containing methods and properties to be applied to the class. However, React mixins are no longer supported with classes defined with ES6+ syntax. React has distanced itself from its own mixins solution in favor of higher-order components (HOC). Simply said, HOCs are functions that accept a React class as an argument and return another class. And there’s a lot of power in that. Using HOCs, we can effectively share methods and properties to any class we pass in.
HOCs add, remove, or change props passed to the supplied component. For example, let’s say that every component we want passed to the HOC should get a user
prop. By having the user
prop supplied by the HOC, all concerns relating to fetching and validating a user is offloaded from the components that will ultimately consume the user metadata.
withUsername.js
function withUsername(WrappedComponent) { return class extends React.Component { state = { user: {} } componentWillMount () { // assumes there is a utility getUserInfo() // function to fetch the logged in user info this.setState({ user: getUserInfo() }); } render () { return <WrappedComponent {...this.props} {...this.state.user} />; } } }
The withUsername
HOC can be used to inject a user
prop into any passed component. In our HOC example, we pass only the component to be rendered as a param. However, the HOC pattern is simply a function that renders a new component and that function can have as many params as makes sense for your use case. For example, in our snippet above we pass in a component and pass a user
prop to it. This is populated by a hard-coded getUserInfo()
utility function. But, what if we wanted the HOC to be even more flexible allowing any user retrieval action to be used? We could define withUsername
with two params in its signature where the second is a function used to fetch the user data.
function withUsername(WrappedComponent, getUserInfo) {
Decorator syntax
Before we move forward with additional examples, let’s take a moment to discuss the use of HOC wrappers using decorator syntax. Instead of explicitly calling the HOC with our component-to-be-wrapped like we did above (withUsername(WrappedComponent))
, we can decorate the WrappedComponent
class itself.
If we knew we wanted our Box component class to float, we could “decorate” it with @withFloat
as we define it:
import withFloat from './withFloat'; @withFloat class Box extends React.Component { // ... }
Decorators are not yet a ratified JavaScript specification. However, with tools like Babel, we can take advantage of decorators today. The payoff is improved readability, particularly when adding multiple HOCs to a class.
Note: If you want to use decorators and are using create-react-app to develop your application, you will have to eject using the npm run eject
command. This is required to add a plugin to Babel. After ejecting you may need to run npm install
once more to update dependencies.
After ejecting (as necessary), install the Babel plugin using npm install --saveDev babel-plugin-transform-decorators-legacy
and then add the following babel
object in the package.json
:
"babel": { "plugins": [ "transform-decorators-legacy" ], "presets": [ "react-app" ] },
Decorators are now able to be transpiled by Babel and used within your project.
Masking components
Let’s take a look at a more practical scenario where we might want to “decorate” a component. Let’s say we want to make a component “maskable.” In Ext JS, the Component class had a mask()
method that would mask off our component’s element and optionally display a message within the mask (often a loading message as remote content was fetched asynchronously). With HOCs, we’re able to impart component markup to our wrapped class. Let’s first take a look at a component we want to mask while content is loaded:
@withMasking class App extends Component { static defaultProps = { onBeforeLoad: () => {}, onLoad: () => {} } componentDidMount () { this.load(); } load () { this.props.onBeforeLoad(); // fetch remote content for this component // call onLoadEnd when done // we’re faking the request with a setTimeout() // to simulate the round trip time setTimeout(() => { this.props.onLoad() }, 3000); // 3 seconds } onLoad () {} render () { return <div style={{width: "200px", height: "200px" }}></div>; } }
Masking HOC
In this oversimplified example, we define a class that calls its load
method when mounted and renders a div
. But, there’s no masking implementation to be found. Let’s create an HOC function to wrap our component with a maskable element and include the necessary masking logic:
function withMasking(WrappedComponent) { return class extends Component { state = { mask: this.props.mask, maskMsg: this.props.maskMsg } onBeforeLoad = () => { this.setState({ mask: true, maskMsg: 'loading...' }); } onLoad = () => { this.setState({ mask: null, maskMsg: null }); } render() { const { onBeforeLoad, onLoad, props } = this; const methodProps = { onBeforeLoad, onLoad }; const { mask, maskMsg } = this.state; return ( <div style={style.wrap}> <WrappedComponent {...methodProps} {...props} /> {mask && <div style={style.modal}> {maskMsg && <div style={style.msg}>{maskMsg}</div>} </div>} </div> ); } } }
The HOC function sets up the state object (using props as defaults). The onBeforeLoad
method sets the mask
and maskMsg
state used by the elements in the render method (with the onLoadEnd
method nullifying the mask data). The render method wraps the passed-in component in a div
used for masking. If a mask
prop is passed, a masking element is rendered using the modal
node of the style object. If the maskMsg
prop is passed we include a text node as well. The ability to mask using props makes our withMasking
HOC that much more flexible.
Masking style
For completeness, below is the CSS style used for masking. In this example we’ve included all styles in an object that lives in the same file as the withMasking
HOC:
const style = { wrap : { display : 'inline-block', position : 'relative' }, modal : { position : 'absolute', top : 0, left : 0, right : 0, bottom : 0, background : 'rgba(0, 0, 0, .3)', display : 'flex', alignItems : 'center', justifyContent : 'center' }, msg : { padding : '8px 16px', border : '1px solid #555', background : 'rgba(0, 0, 0, .1)', color : '#333' } };
Note: To fully enable testing, it’s wise to define and export both the target class and the HOC- decorated class. That way, the undecorated class can be tested independent of the decorating logic. However, it’s not possible to export both the enhanced and original class when using decorator syntax without decorating a class that extends the original class. That may be a consideration when choosing the pattern you use in your code since inheriting from user-classes is not preferred in React.
render
props
HOCs are an established pattern that you’ll likely find in volume in existing code. However, a new pattern is emerging in addition to HOCs: render props. The render
props pattern prevents collisions between props of the same name on both the HOC and the target component. render
props components are composed directly in the application’s JSX the same as with other components. Additionally, it prevents the need to copy over any static methods you may have defined on the component passed to the HOC. The render
props pattern can be seen used in the wild within the react-router and react-motion libraries.
Network view
Before we look at an example of render
props in action, let’s create a simple <NetworkView>
that will perform an onBeforeLoad
, load
, and onLoad
action similar to the <App>
view in our previous example:
import React, {Component} from 'react' class NetworkView extends Component { state = { ip: this.props.ip, mask: this.props.mask, router: this.props.router } static defaultProps = { onBeforeLoad: () => {}, onLoad: () => {} } componentDidMount () { if (!this.props.ip) { this.load(); } } load () { this.props.onBeforeLoad(); setTimeout(() => { this.setState({ ip: '192.168.1.2', mask: '255.255.255.0', router: '192.168.1.1' }); this.props.onLoad() }, 3000); } onLoad () {} render () { const { ip, mask, router } = this.state; return ( <div style={{width: "300px", padding: "12px" }}> <h1>TCP/IP</h1> IP: {ip}<br/> Network Mask: {mask}<br/> Router: {router} </div> ); } } export default NetworkView;
Our <Network>
component accepts five props: ip
, mask
, router
, onBeforeLoad
, and onLoad
. Here we see the first benefit of using the render
prop pattern. Our Network component and our masking component, which we’ll take a look at next, both accept a mask
prop. The HOC pattern would have introduced a conflict between these props that we would need to account for. The render
props pattern circumvents this conflict in prop names altogether, since both the wrapping and the wrapped component are composed in the JSX separately.
Masking class
Let’s take a look at the <Mask>
component we’ll use to mask our Network component’s HTML:
import React, { Component } from 'react' const style = { wrap : { display : 'inline-block', position : 'relative' }, modal : { position : 'absolute', top : 0, left : 0, right : 0, bottom : 0, background : 'rgba(0, 0, 0, .3)', display : 'flex', alignItems : 'center', justifyContent : 'center' }, msg : { padding : '8px 16px', border : '1px solid #555', background : 'rgba(0, 0, 0, .1)', color : '#333' } }; class Mask extends Component { state = { mask: this.props.mask, maskMsg: this.props.maskMsg } onBeforeLoad = () => { this.setState({ mask: true, maskMsg: 'loading...' }); } onLoad = () => { this.setState({ mask: null, maskMsg: null }); } render() { const { onBeforeLoad, onLoad } = this; const { mask, maskMsg } = this.state; return ( <div style={style.wrap}> {this.props.render({onBeforeLoad, onLoad})} {mask && <div style={style.modal}> {maskMsg && <div style={style.msg}>{maskMsg}</div>} </div>} </div> ); } } export default Mask;
Our Mask component looks almost identical to the class returned by the withMasking
function we saw before. The primary difference is that this component renders child components using:
{this.props.render(onBeforeLoad, onLoad)}
Any JSX returned by the <Mask>
render function has access to the params passed (onBeforeLoad
and onLoad
in this case). The convention is to use the prop “render
“, but in reality it could be any prop name you choose. And the params passed to the render
prop are completely flexible as well. Here we pass down methods from our Mask component, but we could have passed in the Mask component’s state, props, or any other value benefitting the render
output.
Masking applied
Let’s take a look at the <Mask>
and <NetworkView>
in action:
import React from 'react'; import Mask from './Mask'; import NetworkView from './NetworkView'; import './App.css'; const App = () => ( <Mask render={({onBeforeLoad, onLoad}) => ( <NetworkView onBeforeLoad={onBeforeLoad} onLoad={onLoad} /> )}/> ); export default App;
The masking abstraction works similar to the HOC masking pattern we saw before, but with no prop collision risk and composed neatly in our <App>
JSX. We therefore remove the need for a dedicated class to wrap our view with the shared logic component as seen with HOCs (i.e. export default withMasking(App)
).
The React docs outline the basic usage and benefits of the render
props pattern. Once you see its elegance, we believe you’ll want to incorporate the pattern in your own code going forward. For a deeper look at the same example contrasted against the now-historic mixins option and the HOC pattern, we recommend watching the presentation on render
props from react-router’s own Michael Jackson.
Wrap up
HOCs and render
props are powerful utilities in an application, though they can be a bit confusing. As you get more comfortable with React code, the use cases for HOCs and / or render
props will become more obvious, and you’ll find yourself at home with their more common patterns. Maintaining reusable code will enable your projects to grow with less maintenance overhead as your applications evolve and change over time. As you come across novel and useful HOC and render
prop examples in your own coding experience and research, add your findings in the comments section of this post!
Mitchell Simoens
Related Posts
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Idera’s acquisition of Sencha has…
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…