¿Cómo puedo responder al ancho de un elemento DOM de tamaño automático en React?

86

Tengo una página web compleja que usa componentes React y estoy tratando de convertir la página de un diseño estático a un diseño más sensible y redimensionable. Sin embargo, sigo encontrando limitaciones con React y me pregunto si existe un patrón estándar para manejar estos problemas. En mi caso específico, tengo un componente que se representa como un div con display: table-cell y width: auto.

Desafortunadamente, no puedo consultar el ancho de mi componente, porque no se puede calcular el tamaño de un elemento a menos que esté realmente colocado en el DOM (que tiene el contexto completo con el cual deducir el ancho real renderizado). Además de usar esto para cosas como el posicionamiento relativo del mouse, también lo necesito para establecer correctamente los atributos de ancho en los elementos SVG dentro del componente.

Además, cuando la ventana cambia de tamaño, ¿cómo comunico los cambios de tamaño de un componente a otro durante la instalación? Estamos haciendo todo nuestro renderizado SVG de terceros en shouldComponentUpdate, pero no puede establecer el estado o las propiedades en usted o en otros componentes secundarios dentro de ese método.

¿Existe una forma estándar de lidiar con este problema usando React?

Steve Hollasch
fuente
1
por cierto, ¿estás seguro de que shouldComponentUpdatees el mejor lugar para renderizar SVG? Parece que lo que quieres es componentWillReceivePropso componentWillUpdatesi no render.
Andy
1
Esto puede ser lo que está buscando o no, pero hay una biblioteca excelente para esto: github.com/bvaughn/react-virtualized Eche un vistazo al componente AutoSizer. Administra automáticamente el ancho y / o alto, por lo que no es necesario.
Maggie
@Maggie echa un vistazo a github.com/souporserious/react-measure también, es una biblioteca independiente para este propósito, y no pondría otras cosas no utilizadas en tu paquete de cliente.
Andy
bueno yo respondí una pregunta similar aquí es de alguna manera un enfoque diferente y que te permite decidir qué mostrar dependiendo del tipo de scren (móvil, tablet, ordenador)
Calin ortan
@Maggie Podría estar equivocado sobre esto, pero creo que Auto Sizer siempre intenta llenar su padre, en lugar de detectar el tamaño que ha tomado el niño para adaptarse a su contenido. Ambos son útiles en situaciones ligeramente diferentes
Andy

Respuestas:

65

La solución más práctica es usar una biblioteca para esto como react-medir .

Actualización : ahora hay un gancho personalizado para la detección de cambio de tamaño (que no he probado personalmente): react-resize-awareness . Al ser un gancho personalizado, parece más cómodo de usar que 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>
)

Para comunicar cambios de tamaño entre componentes, puede pasar una onResizedevolución de llamada y almacenar los valores que recibe en algún lugar (la forma estándar de compartir el estado en estos días es usar 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>
  )
}

Cómo enrollar el tuyo si realmente prefieres:

Cree un componente contenedor que maneje la obtención de valores del DOM y escuche los eventos de cambio de tamaño de la ventana (o la detección de cambio de tamaño del componente como lo usa react-measure). Le dice qué accesorios obtener del DOM y proporciona una función de renderizado que toma esos accesorios como un niño.

Lo que renderiza debe montarse antes de que se puedan leer los accesorios DOM; cuando esos accesorios no están disponibles durante el procesamiento inicial, es posible que desee usarlos style={{visibility: 'hidden'}}para que el usuario no pueda verlos antes de que obtenga un diseño calculado por 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 esto, responder a los cambios de ancho es muy 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>
);
Andy
fuente
Una pregunta sobre este enfoque que aún no he investigado es si interfiere con la SSR. Todavía no estoy seguro de cuál sería la mejor manera de manejar ese caso.
Andy
gran explicación, gracias por ser tan completo :) re: SSR, hay una discusión isMounted()aquí: facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Acabo de agregar código para un buen componente de envoltura que elimina la mayor parte del texto estándar para responder a los cambios de DOM, eche un vistazo :)
Andy
1
@Philll_t sí, sería bueno si el DOM facilitara esto. Pero créame, usar esta biblioteca le ahorrará problemas, aunque no es la forma más básica de obtener una medición.
Andy
1
@Philll_t otra cosa de la que se encargan las bibliotecas es el uso ResizeObserver(o un polyfill) para obtener actualizaciones de tamaño de su código de inmediato.
Andy
43

Creo que el método de ciclo de vida que estás buscando es componentDidMount. Los elementos ya se han colocado en el DOM y puede obtener información sobre ellos en el archivo refs.

Por ejemplo:

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

});
Couchand
fuente
1
Tenga cuidado de que offsetWidthno existe actualmente en Firefox.
Christopher Chiche
@ChristopherChiche No creo que eso sea cierto. ¿Qué versión estás ejecutando? Funciona para mí al menos, y la documentación de MDN parece sugerir que se puede asumir: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/…
couchand
1
Bueno, lo estaré, eso es un inconveniente. En cualquier caso, mi ejemplo probablemente fue pobre, ya que de svgtodos modos debe dimensionar explícitamente un elemento. AFAIK para cualquier cosa que esté buscando, el tamaño dinámico del que probablemente pueda confiar offsetWidth.
Couchand
3
Para cualquiera que venga aquí usando React 0.14 o superior, el .getDOMNode () ya no es necesario: facebook.github.io/react/blog/2015/10/07/…
Hamund
2
Si bien este método puede parecer la forma más fácil (y más parecida a jQuery) de acceder al elemento, Facebook ahora dice "Lo desaconsejamos porque las referencias de cadenas tienen algunos problemas, se consideran heredadas y es probable que se eliminen en el futuro releases. Si actualmente está utilizando this.refs.textInput para acceder a las referencias, recomendamos el patrón de devolución de llamada en su lugar ". Debería utilizar una función de devolución de llamada en lugar de una referencia de cadena. Información aquí
ahaurat
22

Alternativamente a la solución Couchand, puede usar findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
fuente
10
Para aclarar: en React <= 0.12.x use component.getDOMNode (), en React> = 0.13.x use React.findDOMNode ()
pxwise
2
@pxwise Aaaaa y ahora, para las referencias de elementos DOM, ni siquiera tiene que usar ninguna función con React 0.14 :)
Andy
5
@pxwise creo que es ReactDOM.findDOMNode()ahora?
ivarni
1
@MichaelScheper Es cierto, hay algo de ES7 en mi código. En mi respuesta actualizada, la react-measuredemostración es (creo) ES6 puro. Es difícil comenzar, seguro ... Pasé por la misma locura durante el último año y medio :)
Andy
2
@MichaelScheper por cierto, puede encontrar alguna guía útil aquí: github.com/gaearon/react-makes-you-sad
Andy
6

Puede usar la biblioteca I que escribí, que monitorea el tamaño de representación de sus componentes y se lo pasa.

Por ejemplo:

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

Demostración: https://react-sizeme-example-esbefmsitg.now.sh/

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

Utiliza un algoritmo optimizado basado en objetos / desplazamiento que tomé prestado de personas mucho más inteligentes que yo. :)

ctrlplusb
fuente
Bien, gracias por compartir. También estaba a punto de crear un repositorio para mi solución personalizada para un 'DimensionPro provideHoC'. Pero ahora voy a darle una oportunidad a este.
larrydahooster
Agradecería cualquier comentario, positivo o negativo :-)
ctrlplusb
Gracias por esto. <Recharts /> requiere que establezca un ancho y alto explícitos, por lo que esto fue muy útil
JP DeVries