Como posso responder à largura de um elemento DOM de dimensionamento automático no React?

86

Tenho uma página da web complexa que usa componentes React e estou tentando converter a página de um layout estático para um layout mais responsivo e redimensionável. No entanto, continuo encontrando limitações com o React e me pergunto se existe um padrão para lidar com esses problemas. No meu caso específico, tenho um componente que é renderizado como um div com display: table-cell e width: auto.

Infelizmente, não posso consultar a largura do meu componente, porque você não pode calcular o tamanho de um elemento a menos que ele seja realmente colocado no DOM (que tem o contexto completo para deduzir a largura real renderizada). Além de usar isso para coisas como posicionamento relativo do mouse, também preciso disso para definir corretamente os atributos de largura em elementos SVG dentro do componente.

Além disso, quando a janela é redimensionada, como comunico as alterações de tamanho de um componente para outro durante a configuração? Estamos fazendo toda a renderização SVG de terceiros em shouldComponentUpdate, mas você não pode definir o estado ou as propriedades em você mesmo ou em outros componentes filhos dentro desse método.

Existe uma maneira padrão de lidar com esse problema usando o React?

Steve Hollasch
fonte
1
A propósito, tem certeza de que shouldComponentUpdateé o melhor lugar para renderizar SVG? Parece que o que você quer é componentWillReceivePropsou componentWillUpdatenão render.
Andy
1
Isso pode ser ou não o que você está procurando, mas há uma excelente biblioteca para isso: github.com/bvaughn/react-virtualized Dê uma olhada no componente AutoSizer. Ele gerencia automaticamente a largura e / ou altura, para que você não precise fazer isso.
Maggie
@Maggie, verifique github.com/souporserious/react-measure também, é uma biblioteca independente para esse propósito e não colocaria outras coisas não utilizadas em seu pacote de cliente.
Andy
Ei, eu respondi a uma pergunta semelhante aqui. É de alguma forma uma abordagem diferente e permite que você decida o que renderizar dependendo do seu tipo de tela (celular, tablet, desktop)
Calin ortan
@Maggie Posso estar errado sobre isso, mas acho que o Auto Sizer sempre tenta preencher seu pai, em vez de detectar o tamanho que o filho assumiu para caber no conteúdo. Ambos são úteis em situações ligeiramente diferentes
Andy

Respostas:

65

A solução mais prática é usar uma biblioteca para essa medida de reação semelhante .

Update : agora existe um gancho personalizado para detecção de redimensionamento (que não tentei pessoalmente): react-resize-aware . Sendo um gancho personalizado, parece mais conveniente de usar do 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 as mudanças de tamanho entre os componentes, você pode passar um onResizeretorno de chamada e armazenar os valores recebidos em algum lugar (a forma padrão de compartilhar o estado atualmente é 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>
  )
}

Como fazer o seu próprio se você realmente preferir:

Crie um componente wrapper que controle a obtenção de valores do DOM e a escuta de eventos de redimensionamento de janela (ou detecção de redimensionamento de componente conforme usado por react-measure). Você diz a ele quais adereços obter do DOM e fornece uma função de renderização tomando esses adereços como filho.

O que você renderiza precisa ser montado antes que os props DOM possam ser lidos; quando esses adereços não estão disponíveis durante a renderização inicial, você pode querer usar de style={{visibility: 'hidden'}}forma que o usuário não possa ver antes de obter um layout 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)}/>;
  }
}

Com isso, responder às mudanças de largura é muito simples:

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
fonte
Uma questão sobre essa abordagem que ainda não investiguei é se ela interfere no SSR. Ainda não tenho certeza de qual seria a melhor maneira de lidar com esse caso.
Andy
ótima explicação, obrigado por ser tão minucioso :) re: SSR, há uma discussão sobre isMounted()aqui: facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Acabei de adicionar o código para um componente de wrapper agradável que tira a maior parte do boilerplate de respostas às alterações do DOM, dê uma olhada :)
Andy
1
@Philll_t sim, seria bom se o DOM tornasse isso mais fácil. Mas acredite em mim, usar esta biblioteca vai lhe poupar problemas, embora não seja a maneira mais básica de obter uma medição.
Andy
1
@Philll_t outra coisa que as bibliotecas cuidam é usar ResizeObserver(ou um polyfill) para obter atualizações de tamanho para seu código imediatamente.
Andy
43

Acho que o método de ciclo de vida que você está procurando é componentDidMount. Os elementos já foram colocados no DOM e você pode obter informações sobre eles no do componente refs.

Por exemplo:

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
fonte
1
Tenha cuidado, offsetWidthpois não existe atualmente no Firefox.
Christopher Chiche
@ChristopherChiche Não acredito que seja verdade. Qual versão você está executando? Funciona pelo menos para mim, e a documentação MDN parece sugerir que pode ser assumido: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/…
couchand
1
Bem, eu ficarei, isso é inconveniente. Em qualquer caso, meu exemplo provavelmente foi pobre, já que você deve dimensionar explicitamente um svgelemento de qualquer maneira. AFAIK para qualquer coisa que você esteja procurando, o tamanho dinâmico de você provavelmente pode confiar offsetWidth.
mês de
3
Para quem vem aqui usando React 0.14 ou superior, o .getDOMNode () não é mais necessário: facebook.github.io/react/blog/2015/10/07/…
Hamund
2
Embora esse método possa parecer a maneira mais fácil (e mais parecida com jQuery) de acessar o elemento, o Facebook agora diz: "Nós não desaconselhamos isso porque os refs de string têm alguns problemas, são considerados legados e provavelmente serão removidos no futuro lançamentos. Se você estiver usando this.refs.textInput para acessar refs, recomendamos o padrão de retorno de chamada " Você deve usar uma função de retorno de chamada em vez de uma string ref. Informações aqui
ahaurat
22

Como alternativa para a solução, você pode usar findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
fonte
10
Para esclarecer: em React <= 0.12.x use component.getDOMNode (), em React> = 0.13.x use React.findDOMNode ()
pxwise
2
@pxwise Aaaaaand agora para referências de elemento DOM você nem mesmo precisa usar nenhuma das funções com React 0.14 :)
Andy
5
@pxwise, acredito que ReactDOM.findDOMNode()agora?
ivarni
1
@MichaelScheper É verdade, há algum ES7 no meu código. Em minha resposta atualizada, a react-measuredemonstração é (eu acho) puro ES6. É difícil começar, com certeza ... Eu passei pela mesma loucura no último ano e meio :)
Andy
2
@MichaelScheper btw, você pode encontrar algumas orientações úteis aqui: github.com/gaearon/react-makes-you-sad
Andy
6

Você pode usar a biblioteca I que escrevi, que monitora o tamanho dos componentes renderizados e passa para você.

Por exemplo:

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

Ele usa um algoritmo de rolagem / objeto otimizado que peguei emprestado de pessoas muito mais inteligentes do que eu. :)

ctrlplusb
fonte
Legal, obrigado por compartilhar. Eu estava prestes a criar um repo para minha solução personalizada para um 'DimensionProvidingHoC' também. Mas agora vou tentar este.
larrydahooster de
Eu agradeceria qualquer feedback, positivo ou negativo :-)
ctrlplusb
Obrigado por isso. <Recharts /> requer que você defina uma largura e altura explícitas, então isso foi muito útil
JP DeVries