Wie kann ich in React auf die Breite eines DOM-Elements mit automatischer Größe reagieren?

86

Ich habe eine komplexe Webseite mit React-Komponenten und versuche, die Seite von einem statischen Layout in ein reaktionsschnelleres, anpassbares Layout zu konvertieren. Ich stoße jedoch immer wieder auf Einschränkungen bei React und frage mich, ob es ein Standardmuster für die Behandlung dieser Probleme gibt. In meinem speziellen Fall habe ich eine Komponente, die als div mit display: table-cell und width: auto gerendert wird.

Leider kann ich die Breite meiner Komponente nicht abfragen, da Sie die Größe eines Elements nur berechnen können, wenn es tatsächlich im DOM platziert ist (das den vollständigen Kontext hat, mit dem die tatsächlich gerenderte Breite abgeleitet werden kann). Neben der Verwendung für Dinge wie die relative Mauspositionierung benötige ich dies auch, um die Breitenattribute für SVG-Elemente innerhalb der Komponente richtig festzulegen.

Wie kommuniziere ich bei der Größenänderung des Fensters während des Setups Größenänderungen von einer Komponente zur anderen? Wir führen alle SVG-Renderings von Drittanbietern in shouldComponentUpdate durch, aber Sie können innerhalb dieser Methode keinen Status oder keine Eigenschaften für sich selbst oder andere untergeordnete Komponenten festlegen.

Gibt es eine Standardmethode, um dieses Problem mit React zu lösen?

Steve Hollasch
quelle
1
Sind Sie sicher, dass dies shouldComponentUpdateder beste Ort ist, um SVG zu rendern? Es klingt wie das, was Sie wollen componentWillReceivePropsoder componentWillUpdatewenn nicht render.
Andy
1
Dies ist möglicherweise das, wonach Sie suchen, aber dafür gibt es eine hervorragende Bibliothek: github.com/bvaughn/react-virtualized Schauen Sie sich die AutoSizer-Komponente an. Es verwaltet automatisch Breite und / oder Höhe, sodass Sie dies nicht tun müssen.
Maggie
@Maggie Schauen Sie sich auch github.com/souporserious/react-measure an . Es ist eine eigenständige Bibliothek für diesen Zweck und würde keine anderen nicht verwendeten Inhalte in Ihr Client-Bundle aufnehmen.
Andy
hey , antwortete ich eine ähnliche Frage hier Es ist irgendwie ein anderer Ansatz , und es lässt Sie entscheiden , was je nach scren Typ zu machen (Mobiltelefon, Tablet, Desktop)
Calin Ortan
@Maggie Ich könnte mich irren, aber ich denke, Auto Sizer versucht immer, das übergeordnete Element zu füllen, anstatt zu erkennen, welche Größe das Kind angenommen hat, um es an den Inhalt anzupassen. Beide sind in leicht unterschiedlichen Situationen nützlich
Andy

Antworten:

65

Die praktischste Lösung besteht darin, eine Bibliothek für diese Art der Reaktionsmessung zu verwenden .

Update : Es gibt jetzt einen benutzerdefinierten Hook für die Größenänderungserkennung (den ich nicht persönlich ausprobiert habe): React-Resize-Aware . Als benutzerdefinierter Haken sieht es bequemer aus als 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>
)

Um Größenänderungen zwischen Komponenten zu kommunizieren, können Sie einen onResizeRückruf übergeben und die empfangenen Werte irgendwo speichern (die Standardmethode für den Freigabestatus ist heutzutage die Verwendung von 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>
  )
}

So rollen Sie Ihre eigenen, wenn Sie es wirklich vorziehen:

Erstellen Sie eine Wrapper-Komponente, die das Abrufen von Werten aus dem DOM und das Abhören von Ereignissen zur Größenänderung von Fenstern (oder zur Erkennung der Größenänderung von Komponenten, wie sie von verwendet werden react-measure) behandelt. Sie teilen ihm mit, welche Requisiten aus dem DOM abgerufen werden sollen, und stellen eine Renderfunktion bereit, die diese Requisiten als Kind übernimmt.

Was Sie rendern, muss gemountet werden, bevor die DOM-Requisiten gelesen werden können. Wenn diese Requisiten beim ersten Rendern nicht verfügbar sind, möchten Sie sie möglicherweise verwenden, style={{visibility: 'hidden'}}damit der Benutzer sie nicht sehen kann, bevor er ein JS-berechnetes Layout erhält.

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

Damit ist es sehr einfach, auf Breitenänderungen zu reagieren:

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>
);
Andy
quelle
Eine Frage zu diesem Ansatz, die ich noch nicht untersucht habe, ist, ob er die SSR stört. Ich bin mir noch nicht sicher, wie ich diesen Fall am besten behandeln könnte.
Andy
tolle Erklärung, danke, dass isMounted()
du
1
@memeLab Ich habe gerade Code für eine nette Wrapper-Komponente hinzugefügt, die den größten Teil der Boilerplate aus der Reaktion auf DOM-Änderungen herausholt, werfen Sie einen Blick :)
Andy
1
@Philll_t ja es wäre schön wenn das DOM das einfacher machen würde. Aber glauben Sie mir, die Verwendung dieser Bibliothek erspart Ihnen Ärger, auch wenn dies nicht der einfachste Weg ist, eine Messung durchzuführen.
Andy
1
@Philll_t Eine andere Sache, um die sich Bibliotheken kümmern, ist die Verwendung ResizeObserver(oder eine Polyfüllung), um Größenaktualisierungen für Ihren Code sofort zu erhalten.
Andy
43

Ich denke, die Lebenszyklusmethode, nach der Sie suchen, ist componentDidMount. Die Elemente wurden bereits im DOM platziert und Sie können Informationen über sie aus den Komponenten abrufen refs.

Zum Beispiel:

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" />
  }

});
Couchund
quelle
1
Beachten Sie, dass offsetWidthdies derzeit in Firefox nicht vorhanden ist.
Christopher Chiche
@ChristopherChiche Ich glaube nicht, dass das stimmt. Welche Version laufen Sie? Zumindest für mich funktioniert es, und die MDN-Dokumentation scheint darauf
hinzudeuten
1
Nun werde ich sein, das ist unpraktisch. In jedem Fall war mein Beispiel wahrscheinlich ein schlechtes, da Sie ein svgElement sowieso explizit in der Größe anpassen müssen. AFAIK für alles, was Sie suchen, auf dessen dynamische Größe Sie sich wahrscheinlich verlassen können offsetWidth.
Couch und
3
Für alle, die mit React 0.14 oder höher hierher kommen, wird der .getDOMNode () nicht mehr benötigt: facebook.github.io/react/blog/2015/10/07/…
Hamund
2
Während sich diese Methode als die einfachste (und am ehesten jQuery) Möglichkeit anfühlt, auf das Element zuzugreifen, sagt Facebook jetzt: "Wir raten davon ab, da String-Refs einige Probleme haben, als Legacy gelten und wahrscheinlich in einer Zukunft entfernt werden." Releases. Wenn Sie derzeit this.refs.textInput verwenden, um auf Refs zuzugreifen, empfehlen wir stattdessen das Rückrufmuster ". Sie sollten eine Rückruffunktion anstelle einer Zeichenfolgenreferenz verwenden. Info hier
ahaurat
22

Alternativ zu Couch und Lösung können Sie findDOMNode verwenden

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
quelle
10
Zur Verdeutlichung: Verwenden Sie in React <= 0.12.x component.getDOMNode (), in React> = 0.13.x React.findDOMNode ()
pxwise
2
@pxwise Aaaaaund jetzt für DOM-Element-Refs müssen Sie nicht einmal eine der beiden Funktionen mit React 0.14 verwenden :)
Andy
5
@pxwise Ich glaube, es ist ReactDOM.findDOMNode()jetzt?
ivarni
1
@ MichaelScheper Es ist wahr, es gibt einige ES7 in meinem Code. In meiner aktualisierten Antwort ist die react-measureDemonstration (glaube ich) reines ES6. Es ist sicher schwierig anzufangen ... Ich habe in den letzten anderthalb Jahren den gleichen Wahnsinn
Andy
2
@ MichaelScheper übrigens, vielleicht finden Sie hier einige nützliche Anleitungen: github.com/gaearon/react-makes-you-sad
Andy
6

Sie können die von mir geschriebene Bibliothek verwenden, die die gerenderte Größe Ihrer Komponenten überwacht und an Sie weiterleitet.

Beispielsweise:

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

Es verwendet einen optimierten scroll- / objektbasierten Algorithmus, den ich von Leuten ausgeliehen habe, die viel schlauer sind als ich. :) :)

ctrlplusb
quelle
Schön, danke fürs Teilen. Ich wollte gerade ein Repo für meine benutzerdefinierte Lösung für ein 'DimensionProvidingHoC' erstellen. Aber jetzt werde ich es versuchen.
Larrydahooster
Ich würde mich über jede positive oder negative Rückmeldung
freuen
Danke dafür. <Recharts /> erfordert, dass Sie eine explizite Breite und Höhe festlegen, so dass dies sehr hilfreich war
JP DeVries