Skip to content

Instantly share code, notes, and snippets.

@trotzig
Last active March 31, 2017 20:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trotzig/56d1232ff48c14514371934d72fd59dd to your computer and use it in GitHub Desktop.
Save trotzig/56d1232ff48c14514371934d72fd59dd to your computer and use it in GitHub Desktop.
import { addEventListener, removeEventListener } from 'consolidated-events';
import React, { PureComponent } from 'react';
/**
* HoC that injects a `contextWidth` prop to the component, equal to the
* available width in the current context
*
* @param {Object} Component
* @return {Object} a wrapped Component
*/
export default function withContextWidth(Component) {
return class extends PureComponent {
constructor() {
super();
const initialState = {
contextWidth: undefined,
};
this.state = initialState;
this._handleDivRef = this._handleDivRef.bind(this);
this._resizeHandle = addEventListener(
window,
'resize',
() => this.setState(initialState),
{ passive: true },
);
}
componentWillUnmount() {
removeEventListener(this._resizeHandle);
}
_handleDivRef(domElement) {
if (!domElement) {
return;
}
this.setState({
contextWidth: domElement.offsetWidth,
});
}
render() {
if (typeof this.state.contextWidth === 'undefined') {
// This div will live in the document for a brief moment, just long
// enough for it to mount. We then use it to calculate its width, and
// replace it immediately with the underlying component.
return (
<div
style={{ flexGrow: '1' }}
ref={this._handleDivRef}
/>
);
}
return (
<Component
contextWidth={this.state.contextWidth}
{...this.props}
/>
);
}
};
}
@trotzig
Copy link
Author

trotzig commented Mar 31, 2017

Truly responsive React components

Using media queries to style components differently depending on the screen width is great if you're only working in a single column. But let's say you have a multi-column layout where you want responsive components based on the available width in the current container? Or you want a component to be able to render in a lot of different contexts, with unknown widths? With regular media-queries, you can't do that.

withContextWidth is a HOC that will inject a contextWidth prop to the wrapped component. It will allow you to write components that render differently based on the currently available width. Here's an example -- a ToggleButton that collapses to a checkbox in narrow contexts.

function ToggleButton({ 
  downLabel, 
  upLabel,
  isDown,
  contextWidth,
 }) {
  if (contextWidth < 50) {
    return <input type="checkbox" value={isDown} />;
  }
  return (
    <div>
      <button disabled={isDown}>{downLabel}</button>
      <button disabled={!isDown}>{upLabel}</button>
    </div>
  );   
}
export default withContextWidth(ToggleButton);

What's great here is that we can reuse this component in many contexts. If it's rendered in a table for instance, it's likely to render as a checkbox. But if it's a standalone component in a wide container, it's probably going to show the regular, wider version.

How does it work?

To figure out the available width in the current context, we drop an empty <div> in the DOM for a brief moment. As soon as the div is mounted, we measure its width, then re-render with the calculated width injected as contextWidth to the component. The component can then render things conditionally based on this number.

I thought about ways to avoid the empty div but frankly couldn't come up with one that didn't have worse complexity and/or side-effects than the current approach. Let me know if you have ideas!

Limitations

This is still in an early exploration phase, so I expect more limitations/bugs to pop up later on. So far, I've only found issue:

  • The HOC will re-render if the window is resized, but not if the container changes due to some other event (reflowing for instance). This is probably fine for most cases.

Did you find this useful? Drop a comment and let me know what you think!

@trotzig
Copy link
Author

trotzig commented Mar 31, 2017

Don't let the flexGrow: '1' alarm you. The HOC isn't depending on being in a flex container. The flexGrow is there to make sure that if it happens to render in a flexbox parent, it grows as much as it can.

@trotzig
Copy link
Author

trotzig commented Mar 31, 2017

@lencioni just pointed me at http://elementqueries.com/, which is basically what I'm trying to do here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment