Come posso rispondere alla larghezza di un elemento DOM ridimensionato automaticamente in React?


86

Ho una pagina web complessa che utilizza componenti React e sto cercando di convertire la pagina da un layout statico a un layout più reattivo e ridimensionabile. Tuttavia, continuo a incontrare limitazioni con React e mi chiedo se esista uno schema standard per gestire questi problemi. Nel mio caso specifico, ho un componente che viene visualizzato come div con display: table-cell e width: auto.

Sfortunatamente, non posso interrogare la larghezza del mio componente, perché non puoi calcolare la dimensione di un elemento a meno che non sia effettivamente posizionato nel DOM (che ha il contesto completo con cui dedurre l'effettiva larghezza di rendering). Oltre a usarlo per cose come il posizionamento relativo del mouse, ho anche bisogno di questo per impostare correttamente gli attributi di larghezza sugli elementi SVG all'interno del componente.

Inoltre, quando la finestra si ridimensiona, come comunico le modifiche alle dimensioni da un componente a un altro durante l'installazione? Stiamo eseguendo tutto il nostro rendering SVG di terze parti in shouldComponentUpdate, ma non puoi impostare lo stato o le proprietà su te stesso o su altri componenti figlio all'interno di quel metodo.

Esiste un modo standard per affrontare questo problema utilizzando React?


1
a proposito, sei sicuro che shouldComponentUpdatesia il posto migliore per rendere SVG? Sembra che quello che vuoi è componentWillReceivePropso componentWillUpdatese no render.
Andy

1
Questo può essere o meno quello che stai cercando, ma c'è un'eccellente libreria per questo: github.com/bvaughn/react-virtualized Dai un'occhiata al componente AutoSizer. Gestisce automaticamente la larghezza e / o l'altezza, quindi non devi farlo tu.
Maggie

@Maggie controlla anche github.com/souporserious/react-measure , è una libreria standalone per questo scopo e non metterebbe altre cose inutilizzate nel tuo pacchetto client.
Andy

hey ho risposto a una domanda simile qui È in qualche modo un approccio diverso e ti consente di decidere cosa rendere a seconda del tipo di schermo (mobile, tablet, desktop)
Calin ortan

@Maggie Potrei sbagliarmi su questo, ma penso che Auto Sizer cerchi sempre di riempire il suo genitore, piuttosto che rilevare la dimensione che il bambino ha adattato al suo contenuto. Entrambi sono utili in situazioni leggermente diverse
Andy

Risposte:


65

La soluzione più pratica è usare una libreria per questo tipo di misura-reazione .

Aggiornamento : ora c'è un hook personalizzato per il rilevamento del ridimensionamento (che non ho provato personalmente): react-resize-aware . Essendo un gancio personalizzato, sembra più comodo da usare rispetto a 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>
)

Per comunicare le modifiche alle dimensioni tra i componenti, puoi passare una onResizerichiamata e memorizzare i valori che riceve da qualche parte (il modo standard di condividere lo stato in questi giorni è usare 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>
  )
}

Come rollare il tuo se preferisci davvero:

Crea un componente wrapper che gestisce l'acquisizione di valori dal DOM e l'ascolto degli eventi di ridimensionamento della finestra (o il rilevamento del ridimensionamento del componente utilizzato da react-measure). Gli dici quali oggetti di scena ottenere dal DOM e fornisci una funzione di rendering che li prenda da bambino.

Quello che renderizzi deve essere montato prima che gli oggetti di scena DOM possano essere letti; quando questi oggetti di scena non sono disponibili durante il rendering iniziale, potresti volerlo usare in style={{visibility: 'hidden'}}modo che l'utente non possa vederlo prima che ottenga un layout calcolato da 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)}/>;
  }
}

Con questo, rispondere ai cambiamenti di larghezza è molto semplice:

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

Una domanda su questo approccio che non ho ancora studiato è se interferisce con l'SSR. Non sono ancora sicuro di quale sarebbe il modo migliore per gestire quel caso.
Andy

ottima spiegazione, grazie per essere così esauriente :) re: SSR, C'è una discussione di isMounted()qui: facebook.github.io/react/blog/2015/12/16/…
ptim

1
@memeLab Ho appena aggiunto il codice per un bel componente wrapper che elimina la maggior parte del boilerplate dalla risposta alle modifiche DOM, dai un'occhiata :)
Andy

1
@Philll_t sì, sarebbe bello se il DOM lo rendesse più semplice. Ma credimi, l'uso di questa libreria ti farà risparmiare problemi, anche se non è il modo più semplice per ottenere una misurazione.
Andy

1
@Philll_t un'altra cosa di cui si prendono cura le librerie è usare ResizeObserver(o un polyfill) per ottenere immediatamente gli aggiornamenti delle dimensioni del codice.
Andy

43

Penso che il metodo del ciclo di vita che stai cercando sia componentDidMount. Gli elementi sono già stati inseriti nel DOM e puoi ottenere informazioni su di essi dai componenti refs.

Per esempio:

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

});

1
Attenzione che offsetWidthattualmente non esiste in Firefox.
Christopher Chiche

@ChristopherChiche Non credo che sia vero. Che versione stai usando? Funziona almeno per me e la documentazione MDN sembra suggerire che si possa presumere: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/…
couchand

1
Be ', lo sarò, è scomodo. In ogni caso il mio esempio è stato probabilmente mediocre, poiché devi svgcomunque dimensionare esplicitamente un elemento. AFAIK per qualsiasi cosa tu stia cercando, la dimensione dinamica di cui probabilmente puoi fare affidamento offsetWidth.
Couchand

3
Per chiunque venga qui utilizzando React 0.14 o superiore, il .getDOMNode () non è più necessario: facebook.github.io/react/blog/2015/10/07/…
Hamund

2
Sebbene questo metodo possa sembrare il modo più semplice (e più simile a jQuery) per accedere all'elemento, Facebook ora dice "Lo sconsigliamo perché i ref di stringa hanno alcuni problemi, sono considerati legacy e probabilmente verranno rimossi in uno dei futuri rilasci. Se stai attualmente utilizzando this.refs.textInput per accedere a refs, ti consigliamo invece il pattern di callback ". Dovresti usare una funzione di callback invece di una stringa ref. Info qui
ahaurat

22

In alternativa alla soluzione couchand puoi usare findDOMNode

var Container = React.createComponent({

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

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

10
Per chiarire: in React <= 0.12.x usa component.getDOMNode (), in React> = 0.13.x usa React.findDOMNode ()
pxwise

2
@pxwise Aaaaa e ora per gli elementi DOM non devi nemmeno usare nessuna delle due funzioni con React 0.14 :)
Andy

5
@pxwise credo che sia ReactDOM.findDOMNode()adesso?
ivarni

1
@MichaelScheper È vero, c'è dell'ES7 nel mio codice. Nella mia risposta aggiornata la react-measuredimostrazione è (credo) pura ES6. È difficile iniziare, di sicuro ... ho attraversato la stessa follia nell'ultimo anno e mezzo :)
Andy

2
@MichaelScheper btw, potresti trovare alcune indicazioni utili qui: github.com/gaearon/react-makes-you-sad
Andy

6

Puoi usare la libreria che ho scritto che monitora le dimensioni di rendering dei tuoi componenti e te le passa.

Per esempio:

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

Utilizza un algoritmo di scorrimento / oggetto ottimizzato che ho preso in prestito da persone molto più intelligenti di me. :)


Grazie per la condivisione. Stavo per creare un repository per la mia soluzione personalizzata anche per un "DimensionProvidingHoC". Ma ora proverò questo.
larrydahooster

Apprezzerei qualsiasi feedback, positivo o negativo :-)
ctrlplusb

Grazie per questo. <Recharts /> richiede di impostare una larghezza e un'altezza esplicite, quindi è stato molto utile
JP DeVries
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.