I have a complex web page using React components, and am trying to convert the page from a static layout to a more responsive, resizable layout. However, I keep running into
The most practical solution is to use a library for this like react-measure.
Update: there is now a custom hook for resize detection (which I have not tried personally): react-resize-aware. Being a custom hook, it looks more convenient to use than react-measure.
import * as React from 'react'
import Measure from 'react-measure'
const MeasuredComp = () => (
{({ measureRef, contentRect: { bounds: { width }} }) => (
My width is {width}
)}
)
To communicate size changes between components, you can pass an onResize callback and store the values it receives somewhere (the standard way of sharing state these days is to use Redux):
import * as React from 'react'
import Measure from 'react-measure'
import { useSelector, useDispatch } from 'react-redux'
import { setMyCompWidth } from './actions' // some action that stores width in somewhere in redux state
export default function MyComp(props) {
const width = useSelector(state => state.myCompWidth)
const dispatch = useDispatch()
const handleResize = React.useCallback(
(({ contentRect })) => dispatch(setMyCompWidth(contentRect.bounds.width)),
[dispatch]
)
return (
{({ measureRef }) => (
MyComp width is {width}
)}
)
}
How to roll your own if you really prefer to:
Create a wrapper component that handles getting values from the DOM and listening to window resize events (or component resize detection as used by react-measure). You tell it which props to get from the DOM and provide a render function taking those props as a child.
What you render has to get mounted before the DOM props can be read; when those props aren't available during the initial render, you might want to use style={{visibility: 'hidden'}} so that the user can't see it before it gets a JS-computed layout.
// @flow
import React, {Component} from 'react';
import shallowEqual from 'shallowequal';
import throttle from 'lodash.throttle';
type DefaultProps = {
component: ReactClass,
};
type Props = {
domProps?: Array,
computedStyleProps?: Array,
children: (state: State) => ?React.Element,
component: ReactClass,
};
type State = {
remeasure: () => void,
computedStyle?: Object,
[domProp: string]: any,
};
export default class Responsive extends Component {
static defaultProps = {
component: 'div',
};
remeasure: () => void = throttle(() => {
const {root} = this;
if (!root) return;
const {domProps, computedStyleProps} = this.props;
const nextState: $Shape = {};
if (domProps) domProps.forEach(prop => nextState[prop] = root[prop]);
if (computedStyleProps) {
nextState.computedStyle = {};
const computedStyle = getComputedStyle(root);
computedStyleProps.forEach(prop =>
nextState.computedStyle[prop] = computedStyle[prop]
);
}
this.setState(nextState);
}, 500);
// put remeasure in state just so that it gets passed to child
// function along with computedStyle and domProps
state: State = {remeasure: this.remeasure};
root: ?Object;
componentDidMount() {
this.remeasure();
this.remeasure.flush();
window.addEventListener('resize', this.remeasure);
}
componentWillReceiveProps(nextProps: Props) {
if (!shallowEqual(this.props.domProps, nextProps.domProps) ||
!shallowEqual(this.props.computedStyleProps, nextProps.computedStyleProps)) {
this.remeasure();
}
}
componentWillUnmount() {
this.remeasure.cancel();
window.removeEventListener('resize', this.remeasure);
}
render(): ?React.Element {
const {props: {children, component: Comp}, state} = this;
return this.root = c} children={children(state)}/>;
}
}
With this, responding to width changes is very simple:
function renderColumns(numColumns: number): React.Element {
...
}
const responsiveView = (
{({offsetWidth}: {offsetWidth: number}): ?React.Element => {
if (!offsetWidth) return null;
const numColumns = Math.max(1, Math.floor(offsetWidth / 200));
return renderColumns(numColumns);
}}
);