在 React 中如何响应自动调整大小的 DOM 元素的宽度?

我有一个复杂的网页使用反应组件,并试图转换的网页从静态布局,以一个更响应,可调整的布局。然而,我在使用 React 时不断遇到限制,我想知道是否存在处理这些问题的标准模式。在我的特定示例中,我有一个以 div 形式呈现的组件,其 display: table-cell 和 width: auto。

不幸的是,我不能查询我的组件的宽度,因为你不能计算一个元素的大小,除非它实际上放在 DOM (它有完整的上下文,用来推断实际呈现的宽度)。除了将其用于诸如相对鼠标定位之类的事情之外,我还需要它来正确地设置组件中 SVG 元素的宽度属性。

另外,当窗口调整大小时,在安装过程中如何将大小变化从一个组件传递给另一个组件?我们所有的第三方 SVG 呈现都是在 should dComponent 更新中完成的,但是您不能在该方法中为您自己或其他子组件设置状态或属性。

有没有一个标准的方法来处理这个问题使用反应?

87845 次浏览

I think the lifecycle method you're looking for is componentDidMount. The elements have already been placed in the DOM and you can get information about them from the component's refs.

For instance:

var Container = React.createComponent({


componentDidMount: function () {
// if using React < 0.14, use this.refs.svg.getDOMNode().offsetWidth
var width = this.refs.svg.offsetWidth;
},


render: function () {
<svg ref="svg" />
}


});

Alternatively to couchand solution you can use findDOMNode

var Container = React.createComponent({


componentDidMount: function () {
var width = React.findDOMNode(this).offsetWidth;
},


render: function () {
<svg />
}
});

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 = () => (
<Measure bounds>
{({ measureRef, contentRect: { bounds: { width }} }) => (
<div ref={measureRef}>My width is {width}</div>
)}
</Measure>
)

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 (
<Measure bounds onResize={handleResize}>
{({ measureRef }) => (
<div ref={measureRef}>MyComp width is {width}</div>
)}
</Measure>
)
}

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<any>,
};


type Props = {
domProps?: Array<string>,
computedStyleProps?: Array<string>,
children: (state: State) => ?React.Element<any>,
component: ReactClass<any>,
};


type State = {
remeasure: () => void,
computedStyle?: Object,
[domProp: string]: any,
};


export default class Responsive extends Component<DefaultProps,Props,State> {
static defaultProps = {
component: 'div',
};


remeasure: () => void = throttle(() => {
const {root} = this;
if (!root) return;
const {domProps, computedStyleProps} = this.props;
const nextState: $Shape<State> = {};
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<any> {
const {props: {children, component: Comp}, state} = this;
return <Comp ref={c => this.root = c} children={children(state)}/>;
}
}

With this, responding to width changes is very simple:

function renderColumns(numColumns: number): React.Element<any> {
...
}
const responsiveView = (
<Responsive domProps={['offsetWidth']}>
{({offsetWidth}: {offsetWidth: number}): ?React.Element<any> => {
if (!offsetWidth) return null;
const numColumns = Math.max(1, Math.floor(offsetWidth / 200));
return renderColumns(numColumns);
}}
</Responsive>
);

You could use I library I wrote which monitors your components rendered size and passes it through to you.

For example:

import SizeMe from 'react-sizeme';


class MySVG extends Component {
render() {
// A size prop is passed into your component by my library.
const { width, height } = this.props.size;


return (
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
);
}
}


// Wrap your component export with my library.
export default SizeMe()(MySVG);

Demo: https://react-sizeme-example-esbefmsitg.now.sh/

Github: https://github.com/ctrlplusb/react-sizeme

It uses an optimised scroll/object based algorithm that I borrowed from people much more clever than I am. :)