Bagaimana saya bisa menanggapi lebar elemen DOM berukuran otomatis di React?

86

Saya memiliki halaman web yang kompleks menggunakan komponen React, dan saya mencoba untuk mengubah halaman dari tata letak statis menjadi tata letak yang lebih responsif dan dapat diubah ukurannya. Namun, saya terus mengalami keterbatasan dengan React, dan bertanya-tanya apakah ada pola standar untuk menangani masalah ini. Dalam kasus khusus saya, saya memiliki komponen yang dirender sebagai div dengan display: table-cell dan width: auto.

Sayangnya, saya tidak dapat menanyakan lebar komponen saya, karena Anda tidak dapat menghitung ukuran elemen kecuali itu benar-benar ditempatkan di DOM (yang memiliki konteks lengkap untuk menyimpulkan lebar yang diberikan sebenarnya). Selain menggunakan ini untuk hal-hal seperti posisi mouse relatif, saya juga membutuhkan ini untuk mengatur atribut lebar dengan benar pada elemen SVG di dalam komponen.

Selain itu, saat jendela berubah ukuran, bagaimana cara mengkomunikasikan perubahan ukuran dari satu komponen ke komponen lainnya selama penyetelan? Kami melakukan semua rendering SVG pihak ketiga kami di shouldComponentUpdate, tetapi Anda tidak dapat menyetel status atau properti pada diri Anda sendiri atau komponen turunan lainnya dalam metode itu.

Apakah ada cara standar untuk mengatasi masalah ini menggunakan React?

Steve Hollasch
sumber
1
Ngomong-ngomong, apakah Anda yakin shouldComponentUpdatetempat terbaik untuk merender SVG? Kedengarannya seperti apa yang Anda inginkan componentWillReceivePropsatau componentWillUpdatejika tidak render.
Andy
1
Ini mungkin atau bukan yang Anda cari, tetapi ada pustaka yang sangat bagus untuk ini: github.com/bvaughn/react-virtualized Lihat komponen AutoSizer. Secara otomatis mengatur lebar dan / atau tinggi, jadi Anda tidak perlu melakukannya.
Maggie
@Maggie lihat juga github.com/souporserious/react-measure , ini adalah pustaka mandiri untuk tujuan ini, dan tidak akan memasukkan hal-hal lain yang tidak terpakai ke dalam bundel klien Anda.
Andy
hei saya menjawab pertanyaan serupa di sini Ini entah bagaimana merupakan pendekatan yang berbeda dan memungkinkan Anda memutuskan apa yang akan dirender tergantung pada jenis scren Anda (seluler, tablet, desktop)
Calin ortan
@Maggie Saya bisa saja salah tentang ini, tetapi menurut saya Auto Sizer selalu mencoba untuk mengisi induknya, daripada mendeteksi ukuran anak yang diambil agar sesuai dengan kontennya. Keduanya berguna dalam situasi yang sedikit berbeda
Andy

Jawaban:

65

Solusi paling praktis adalah menggunakan perpustakaan untuk pengukuran reaksi seperti ini .

Pembaruan : sekarang ada kait khusus untuk deteksi pengubahan ukuran (yang belum saya coba secara pribadi): react-resize-aware . Menjadi pengait khusus, terlihat lebih nyaman digunakan daripada 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>
)

Untuk mengomunikasikan perubahan ukuran antar komponen, Anda dapat meneruskan onResizecallback dan menyimpan nilai yang diterimanya di suatu tempat (cara standar berbagi status saat ini adalah dengan menggunakan 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>
  )
}

Cara menggulung sendiri jika Anda lebih suka:

Buat komponen pembungkus yang menangani pengambilan nilai dari DOM dan mendengarkan peristiwa pengubahan ukuran jendela (atau deteksi pengubahan ukuran komponen seperti yang digunakan oleh react-measure). Anda memberi tahu alat peraga mana yang akan didapat dari DOM dan menyediakan fungsi render yang mengambil alat peraga tersebut sebagai anak.

Apa yang Anda render harus sudah terpasang sebelum alat peraga DOM dapat dibaca; ketika props tersebut tidak tersedia selama rendering awal, Anda mungkin ingin menggunakan style={{visibility: 'hidden'}}sehingga pengguna tidak dapat melihatnya sebelum mendapatkan tata letak yang dihitung 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)}/>;
  }
}

Dengan ini, menanggapi perubahan lebar sangat sederhana:

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
sumber
Satu pertanyaan tentang pendekatan ini yang belum saya selidiki adalah apakah itu mengganggu RSK. Saya belum yakin apa cara terbaik untuk menangani kasus itu.
Andy
penjelasan yang bagus, terima kasih sudah sangat teliti :) re: SSR, Ada diskusi di isMounted()sini: facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Saya baru saja menambahkan kode untuk komponen pembungkus yang bagus yang mengambil sebagian besar boilerplate dari menanggapi perubahan DOM, lihatlah :)
Andy
1
@Philll_t ya alangkah baiknya jika DOM membuat ini lebih mudah. Tapi percayalah, menggunakan pustaka ini akan menyelamatkan Anda dari masalah, meskipun itu bukan cara paling dasar untuk mendapatkan pengukuran.
Andy
1
@Philll_t hal lain yang diurus perpustakaan adalah menggunakan ResizeObserver(atau polyfill) untuk mendapatkan pembaruan ukuran pada kode Anda dengan segera.
Andy
43

Saya pikir metode siklus hidup yang Anda cari adalah componentDidMount. Elemen telah ditempatkan di DOM dan Anda bisa mendapatkan informasi tentang mereka dari komponen refs.

Misalnya:

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
sumber
1
Berhati-hatilah karena offsetWidthsaat ini tidak ada di Firefox.
Christopher Chiche
@ChristopherChiche Saya tidak percaya itu benar. Versi apa yang Anda jalankan? Setidaknya, ini berfungsi untuk saya, dan dokumentasi MDN tampaknya menyarankan bahwa ini dapat diasumsikan: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/…
couchand
1
Baiklah, itu tidak nyaman. Bagaimanapun, contoh saya mungkin salah, karena Anda harus secara eksplisit mengukur svgelemen. AFAIK untuk apa pun yang Anda cari untuk ukuran dinamis yang mungkin dapat Anda andalkan offsetWidth.
sofa dan
3
Bagi siapa pun yang datang ke sini menggunakan React 0.14 atau lebih tinggi, .getDOMNode () tidak lagi diperlukan: facebook.github.io/react/blog/2015/10/07/…
Hamund
2
Meskipun metode ini mungkin terasa seperti cara termudah (dan paling mirip dengan jQuery) untuk mengakses elemen, Facebook sekarang mengatakan "Kami menyarankan untuk tidak melakukannya karena referensi string memiliki beberapa masalah, dianggap lama, dan kemungkinan besar akan dihapus di salah satu masa mendatang. rilis. Jika saat ini Anda menggunakan this.refs.textInput untuk mengakses referensi, kami merekomendasikan pola panggilan balik sebagai gantinya ". Anda harus menggunakan fungsi callback sebagai ganti string ref. Info Di Sini
ahaurat
22

Alternatifnya untuk solusi sofa Anda dapat menggunakan findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Lukasz Madon
sumber
10
Untuk memperjelas: di React <= 0.12.x gunakan component.getDOMNode (), di React> = 0.13.x gunakan React.findDOMNode ()
pxwise
2
@pxwise Aaaaa dan sekarang untuk referensi elemen DOM Anda bahkan tidak perlu menggunakan salah satu fungsi dengan React 0.14 :)
Andy
5
@pxwise saya percaya ReactDOM.findDOMNode()sekarang?
ivarni
1
@MichaelScheper Memang benar, ada beberapa ES7 di kode saya. Dalam jawaban saya yang diperbarui, react-measuredemonstrasinya adalah (menurut saya) ES6 murni. Sulit untuk memulainya, pasti ... Saya mengalami kegilaan yang sama selama satu setengah tahun terakhir :)
Andy
2
@MichaelScheper btw, Anda mungkin menemukan beberapa panduan yang berguna di sini: github.com/gaearon/react-makes-you-sad
Andy
6

Anda dapat menggunakan I library yang saya tulis yang memonitor ukuran komponen yang Anda berikan dan meneruskannya kepada Anda.

Sebagai contoh:

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

Ini menggunakan algoritma berbasis gulir / objek yang dioptimalkan yang saya pinjam dari orang-orang yang jauh lebih pintar daripada saya. :)

ctrlplusb
sumber
Bagus, terima kasih sudah berbagi. Saya akan membuat repo untuk solusi khusus saya untuk 'DimensionProvidingHoC' juga. Tapi sekarang saya akan mencoba yang ini.
larrydahooster
Saya sangat menghargai setiap umpan balik, positif atau negatif :-)
ctrlplusb
Terima kasih untuk ini. <Recharts /> mengharuskan Anda menyetel lebar dan tinggi secara eksplisit, jadi ini sangat membantu
JP DeVries