web-dev-qa-db-fra.com

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

J'ai une page Web complexe utilisant des composants React et je tente de convertir la page d'une présentation statique en une présentation redimensionnable, plus réactive. Cependant, je continue à me heurter à des limites avec React et je me demande s’il existe un modèle standard pour traiter ces problèmes. Dans mon cas spécifique, j'ai un composant qui rend en tant que 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 dispose du contexte complet permettant de déduire la largeur de rendu réelle). Outre son utilisation pour des opérations telles que le positionnement relatif de la souris, j'ai également besoin de cette fonctionnalité pour définir correctement les attributs de largeur des éléments SVG au sein du composant.

En outre, lorsque la fenêtre est redimensionnée, comment puis-je communiquer les modifications de taille d'un composant à un autre lors de l'installation? Nous effectuons tous nos rendus SVG tiers dans shouldComponentUpdate, mais vous ne pouvez pas définir d'état ou de propriétés sur vous-même ou sur d'autres composants enfants au sein de cette méthode.

Existe-t-il un moyen standard de traiter ce problème en utilisant React?

84
Steve Hollasch

La solution la plus pratique consiste à utiliser réaction-mesure .

Remarque : ce code ne fonctionne pas avec react-measure@^2.0.0 car l'API a été modifiée. Visitez le lien ci-dessus pour voir la nouvelle API.

import Measure from 'react-measure'

const MeasuredComp = () => (
  <Measure>
    {({width}) => <div>My width is {width}</div>}
  </Measure>
)

Pour communiquer les modifications de taille entre composants, vous pouvez passer un rappel onMeasure et stocker les valeurs qu'il reçoit quelque part (la méthode standard de partage de l'état ces jours-ci consiste à utiliser Redux ):

import Measure from 'react-measure'
import connect from 'react-redux'
import {setMyCompWidth} from './actions' // some action that stores width in somewhere in redux state

function select(state) {
  return {
    currentWidth: ... // get width from somewhere in the state
  }
}

const MyComp = connect(select)(({dispatch, currentWidth}) => (
  <Measure onMeasure={({width}) => dispatch(setMyCompWidth(width))}>
    <div>MyComp width is {currentWidth}</div>
  </Measure>
))

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

Créez un composant wrapper qui gère l'extraction des valeurs du DOM et l'écoute d'événements de redimensionnement de fenêtre (ou la détection de redimensionnement de composant utilisée par react-measure). Vous lui indiquez les accessoires à obtenir du DOM et vous 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 utiliser style={{visibility: 'hidden'}} afin que l'utilisateur ne puisse pas le voir avant d'avoir 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>
);
64
Andy

Je pense que la méthode de 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 de la variable refs du composant.

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

});
43
couchand

Vous pouvez également utiliser findDOMNode comme solution de rechange.

var Container = React.createComponent({

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

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

Vous pouvez utiliser la bibliothèque que j’ai écrite pour contrôler la taille de rendu de vos composants et pour vous la transmettre.

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 optimisé basé sur le défilement/objet que j'ai emprunté à des personnes beaucoup plus intelligentes que moi. :)

5
ctrlplusb