Comment puis-je répondre à la largeur d'un élément DOM à taille automatique dans React?

86

J'ai une page Web complexe utilisant des composants React et j'essaie de convertir la page d'une mise en page statique en une mise en page plus réactive et redimensionnable. Cependant, je continue à rencontrer des limitations avec React et je me demande s'il existe un modèle standard pour gérer ces problèmes. Dans mon cas particulier, j'ai un composant qui rend comme div avec display: table-cell et width: auto.

Malheureusement, je ne peux pas interroger la largeur de mon composant, car vous ne pouvez pas calculer la taille d'un élément à moins qu'il ne soit réellement placé dans le DOM (qui a le contexte complet avec lequel déduire la largeur réelle du rendu). En plus de l'utiliser pour des choses comme le positionnement relatif de la souris, j'en ai également besoin pour définir correctement les attributs de largeur sur les éléments SVG dans le composant.

De plus, lorsque la fenêtre se redimensionne, comment communiquer les changements de taille d'un composant à un autre pendant l'installation? Nous effectuons tout notre rendu SVG tiers dans shouldComponentUpdate, mais vous ne pouvez pas définir l'état ou les propriétés de vous-même ou d'autres composants enfants dans cette méthode.

Existe-t-il une manière standard de traiter ce problème en utilisant React?

Steve Hollasch
la source
1
au fait, êtes-vous sûr que shouldComponentUpdatec'est le meilleur endroit pour rendre SVG? Cela ressemble à ce que vous voulez componentWillReceivePropsou componentWillUpdatesinon render.
Andy
1
C'est peut-être ce que vous recherchez ou non, mais il existe une excellente bibliothèque pour cela: github.com/bvaughn/react-virtualized Jetez un œil au composant AutoSizer. Il gère automatiquement la largeur et / ou la hauteur, vous n'avez donc pas à le faire.
Maggie
@Maggie consultez aussi github.com/souporserious/react-measure , c'est une bibliothèque autonome à cet effet, et ne mettrait pas d'autres éléments inutilisés dans votre bundle client.
Andy
hey j'ai répondu à une question similaire ici C'est en quelque sorte une approche différente et cela vous laisse décider quoi rendre en fonction de votre type d'écran (mobile, tablette, bureau)
Calin ortan
@Maggie Je peux me tromper à ce sujet, mais je pense que Auto Sizer essaie toujours de remplir son parent, plutôt que de détecter la taille que l'enfant a prise pour s'adapter à son contenu. Les deux sont utiles dans des situations légèrement différentes
Andy

Réponses:

65

La solution la plus pratique consiste à utiliser une bibliothèque pour cela comme react-measure .

Mise à jour : il existe maintenant un hook personnalisé pour la détection de redimensionnement (que je n'ai pas essayé personnellement): react-resize-aware . Étant un crochet personnalisé, il semble plus pratique à utiliser 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>
)

Pour communiquer les changements de taille entre les composants, vous pouvez passer un onResizerappel et stocker les valeurs qu'il reçoit quelque part (le moyen standard de partager l'état de nos jours est d'utiliser 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>
  )
}

Comment rouler le vôtre si vous préférez vraiment:

Créez un composant wrapper qui gère l'obtention des valeurs du DOM et l'écoute des événements de redimensionnement de fenêtre (ou la détection de redimensionnement de composant telle qu'utilisée par react-measure). Vous lui indiquez quels accessoires obtenir du DOM et fournissez une fonction de rendu prenant ces accessoires comme un enfant.

Ce que vous rendez doit être monté avant que les accessoires DOM puissent être lus; lorsque ces accessoires ne sont pas disponibles lors du rendu initial, vous pouvez les utiliser style={{visibility: 'hidden'}}pour que l'utilisateur ne puisse pas les voir avant d'obtenir une mise en page calculée par 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)}/>;
  }
}

Avec cela, répondre aux changements de largeur est très 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
la source
Une question sur cette approche que je n'ai pas encore étudiée est de savoir si elle interfère avec le SSR. Je ne sais pas encore quelle serait la meilleure façon de traiter cette affaire.
Andy
bonne explication, merci d'être si minutieux :) re: SSR, Il y a une discussion isMounted()ici: facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Je viens d'ajouter du code pour un joli composant wrapper qui évite à la plupart du temps de répondre aux changements du DOM, jetez un œil :)
Andy
1
@Philll_t oui ce serait bien si le DOM rendait cela plus facile. Mais croyez-moi, l'utilisation de cette bibliothèque vous évitera des problèmes, même si ce n'est pas le moyen le plus simple d'obtenir une mesure.
Andy
1
@Philll_t une autre chose dont les bibliothèques s'occupent est d'utiliser ResizeObserver(ou un polyfill) pour obtenir des mises à jour de taille de votre code immédiatement.
Andy
43

Je pense que la méthode du cycle de vie que vous recherchez est componentDidMount. Les éléments ont déjà été placés dans le DOM et vous pouvez obtenir des informations à leur sujet à partir du composant refs.

Par exemple:

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
la source
1
Attention, cela offsetWidthn'existe pas actuellement dans Firefox.
Christopher Chiche
@ChristopherChiche Je ne crois pas que ce soit vrai. Quelle version utilisez-vous? Cela fonctionne au moins pour moi, et la documentation MDN semble suggérer que cela peut être supposé: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model
...
1
Eh bien, je le serai, ce n'est pas pratique. En tout cas, mon exemple était probablement médiocre, car vous devez de svgtoute façon dimensionner explicitement un élément. AFAIK pour tout ce que vous recherchez sur la taille dynamique de vous pouvez probablement compter offsetWidth.
couchand
3
Pour toute personne venant ici en utilisant React 0.14 ou supérieur, le .getDOMNode () n'est plus nécessaire: facebook.github.io/react/blog/2015/10/07
...
2
Bien que cette méthode puisse sembler être le moyen le plus simple (et le plus semblable à jQuery) d'accéder à l'élément, Facebook dit maintenant: "Nous vous le déconseillons car les références de chaîne ont des problèmes, sont considérées comme héritées et sont susceptibles d'être supprimées dans le futur. Si vous utilisez actuellement this.refs.textInput pour accéder aux refs, nous vous recommandons plutôt le modèle de rappel ". Vous devez utiliser une fonction de rappel au lieu d'une chaîne ref. Info ici
ahaurat
22

Alternativement à la solution couchand, vous pouvez utiliser findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
la source
10
Pour clarifier: dans React <= 0.12.x utilisez component.getDOMNode (), dans React> = 0.13.x utilisez React.findDOMNode ()
pxwise
2
@pxwise Aaaaa et maintenant pour les références d'éléments DOM, vous n'avez même pas besoin d'utiliser l'une ou l'autre des fonctions avec React 0.14 :)
Andy
5
@pxwise je crois que c'est ReactDOM.findDOMNode()maintenant?
ivarni le
1
@MichaelScheper C'est vrai, il y a du ES7 dans mon code. Dans ma réponse mise à jour, la react-measuredémonstration est (je pense) pure ES6. C'est difficile de commencer, c'est sûr ... J'ai vécu la même folie au cours de la dernière année et demie :)
Andy
2
@MichaelScheper btw, vous trouverez peut-être des conseils utiles ici: github.com/gaearon/react-makes-you-sad
Andy
6

Vous pouvez utiliser la bibliothèque I que j'ai écrite qui surveille la taille du rendu de vos composants et vous la transmet.

Par exemple:

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

Démo: https://react-sizeme-example-esbefmsitg.now.sh/

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

Il utilise un algorithme de défilement / objet optimisé que j'ai emprunté à des personnes beaucoup plus intelligentes que moi. :)

ctrlplusb
la source
Bien, merci pour le partage. J'étais également sur le point de créer un dépôt pour ma solution personnalisée pour un 'DimensionProvidingHoC'. Mais maintenant je vais essayer celui-ci.
larrydahooster
J'apprécierais vos commentaires, positifs ou négatifs :-)
ctrlplusb
Merci pour ça. <Recharts /> vous oblige à définir une largeur et une hauteur explicites, donc cela a été très utile
JP DeVries