1. ホーム
  2. javascript

[解決済み] Reactで、オートサイズのDOM要素の幅に対応するにはどうしたらいいですか?

2023-04-14 01:56:02

質問

私は React コンポーネントを使用した複雑な Web ページを持っており、ページを静的なレイアウトから、より応答性の高い、サイズ変更可能なレイアウトに変換しようとしています。しかし、私はReactの制限に遭遇し続け、これらの問題を処理するための標準的なパターンがあるかどうか疑問に思っています。私の特定のケースでは、div としてレンダリングされるコンポーネントがあり、display:table-cell と width:auto があります。

なぜなら、要素が実際に DOM に配置されない限り、要素のサイズを計算できないからです (実際のレンダリング幅を推論するための完全なコンテキストを持つ)。マウスの相対的な位置決めなどのためにこれを使用するほかに、コンポーネント内の SVG 要素に幅属性を適切に設定するためにも、これが必要です。

さらに、ウィンドウのサイズが変更されたとき、セットアップ中に 1 つのコンポーネントから別のコンポーネントにサイズの変更をどのように伝えるのでしょうか? サードパーティの SVG レンダリングをすべて shouldComponentUpdate で行っていますが、そのメソッド内で自分自身や他の子コンポーネントに状態やプロパティを設定することはできません。

Reactを使用してこの問題に対処する標準的な方法はありますか?

どのように解決するのですか?

最も現実的な解決策は、以下のようなライブラリを使用することです。 リアクトメジャー .

更新 : リサイズ検出用のカスタムフックが追加されました (個人的には試していませんが)。 react-resize-aware . カスタムフックということで、以下のようなものよりも使い勝手がよさそうです。 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>
)

コンポーネント間でサイズの変更を伝達するために onResize コールバックを渡し、それが受け取る値をどこかに保存します (最近、状態を共有する標準的な方法は 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>
  )
}

本当に必要なら、どのように自分でロールバックするか。

DOM から値を取得し、ウィンドウのリサイズイベント (または で使用されるコンポーネントのリサイズ検出) をリスンするラッパーコンポーネントを作成します。 react-measure ). DOM からどのプロップを取得するかを指示し、それらのプロップを子として受け取るレンダー関数を提供します。

レンダリングするものは、DOM のプロップが読み込まれる前にマウントされなければなりません。最初のレンダリング中にこれらのプロップが利用できない場合、レンダリング関数に style={{visibility: 'hidden'}} を使用して、JS で計算されたレイアウトを取得する前にユーザーがそれを見ることができないようにすることができます。

// @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)}/>;
  }
}

これで、幅の変化への対応が非常に簡単になります。

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>
);