Jak mogę odpowiedzieć na szerokość elementu DOM automatycznie dopasowanego do rozmiaru w Reakcie?

86

Mam złożoną stronę internetową używającą komponentów React i próbuję przekonwertować stronę z układu statycznego na bardziej responsywny układ o zmiennym rozmiarze. Jednak wciąż napotykam ograniczenia w React i zastanawiam się, czy istnieje standardowy wzorzec radzenia sobie z tymi problemami. W moim konkretnym przypadku mam komponent, który renderuje się jako div z wyświetlaczem: table-cell i width: auto.

Niestety, nie mogę zapytać o szerokość mojego komponentu, ponieważ nie możesz obliczyć rozmiaru elementu, chyba że jest on faktycznie umieszczony w DOM (który ma pełny kontekst, z którego można wywnioskować rzeczywistą renderowaną szerokość). Oprócz używania tego do takich rzeczy, jak względne pozycjonowanie myszy, potrzebuję również tego do prawidłowego ustawienia atrybutów szerokości elementów SVG w komponencie.

Ponadto, gdy zmienia się rozmiar okna, w jaki sposób mogę przekazywać zmiany rozmiaru między komponentami podczas instalacji? Wykonujemy wszystkie renderowanie SVG innych firm w shouldComponentUpdate, ale nie możesz ustawić stanu ani właściwości dla siebie ani innych komponentów podrzędnych w tej metodzie.

Czy istnieje standardowy sposób radzenia sobie z tym problemem za pomocą Reacta?

Steve Hollasch
źródło
1
tak przy okazji, czy na pewno shouldComponentUpdatejest to najlepsze miejsce do renderowania SVG? Wygląda na to, że chcesz, componentWillReceivePropsa componentWillUpdatejeśli nie render.
Andy
1
Może to być to, czego szukasz, ale jest do tego doskonała biblioteka: github.com/bvaughn/react-virtualized Spójrz na składnik AutoSizer . Automatycznie zarządza szerokością i / lub wysokością, więc nie musisz tego robić.
Maggie
@Maggie sprawdź również github.com/souporserious/react-measure , jest to samodzielna biblioteka do tego celu i nie umieszczałaby innych nieużywanych rzeczy w pakiecie klienta.
Andy
hej odpowiedział podobne pytanie tutaj To jakoś odmienne podejście i niech Ci zdecydować, co czynią w zależności od typu (scren mobilne, tablety, netbooki)
Calin ortan
@Maggie Mogę się mylić co do tego, ale myślę, że Auto Sizer zawsze stara się wypełnić swojego rodzica, zamiast wykrywać rozmiar, jaki dziecko zajęło, aby dopasować jego zawartość. Oba są przydatne w nieco innych sytuacjach
Andy

Odpowiedzi:

65

Najbardziej praktycznym rozwiązaniem jest skorzystanie z biblioteki do tego typu pomiaru reakcji .

Aktualizacja : istnieje teraz niestandardowy punkt zaczepienia do wykrywania zmiany rozmiaru (którego osobiście nie próbowałem): reakcja-zmiana rozmiaru-świadomość . Będąc niestandardowym haczykiem, wygląda na wygodniejszy w użyciu niż 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>
)

Aby przekazać zmiany rozmiaru między komponentami, możesz przekazać onResizewywołanie zwrotne i gdzieś zapisać otrzymane wartości (obecnie standardowym sposobem udostępniania stanu jest użycie 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>
  )
}

Jak rzucić własne, jeśli naprawdę wolisz:

Utwórz komponent opakowujący, który obsługuje pobieranie wartości z DOM i nasłuchiwanie zdarzeń zmiany rozmiaru okna (lub wykrywania zmiany rozmiaru komponentu, jak jest używane przez react-measure). Mówisz mu, które rekwizyty pobrać z DOM-a i udostępniasz funkcję renderującą, która przyjmuje te właściwości jako dziecko.

To, co renderujesz, musi zostać zamontowane, zanim będzie można odczytać właściwości DOM; kiedy te właściwości nie są dostępne podczas początkowego renderowania, możesz zechcieć ich użyć, style={{visibility: 'hidden'}}aby użytkownik nie mógł ich zobaczyć, zanim otrzyma układ obliczony w 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)}/>;
  }
}

Dzięki temu reagowanie na zmiany szerokości jest bardzo proste:

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
źródło
Jedno pytanie dotyczące tego podejścia, którego jeszcze nie zbadałem, dotyczy tego, czy koliduje z SSR. Nie jestem jeszcze pewien, jaki byłby najlepszy sposób załatwienia tej sprawy.
Andy
świetne wyjaśnienie, dzięki za dokładność :) re: SSR, Dyskusja o tym isMounted()tutaj: facebook.github.io/react/blog/2015/12/16/…
ptim
1
@memeLab Właśnie dodałem kod dla ładnego komponentu opakowującego, który usuwa większość standardowego sposobu reagowania na zmiany DOM, spójrz :)
Andy
1
@Philll_t tak, byłoby miło, gdyby DOM to ułatwił. Ale zaufaj mi, korzystanie z tej biblioteki pozwoli Ci zaoszczędzić kłopotów, nawet jeśli nie jest to najbardziej podstawowy sposób uzyskania pomiaru.
Andy
1
@Philll_t kolejną rzeczą, o którą dbają biblioteki, jest używanie ResizeObserver(lub polyfill) do natychmiastowego pobierania aktualizacji rozmiaru do twojego kodu.
Andy
43

Myślę, że metoda cyklu życia, której szukasz, to componentDidMount. Elementy zostały już umieszczone w DOM i możesz uzyskać informacje o nich z komponentu refs.

Na przykład:

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
źródło
1
Uważaj, że offsetWidthobecnie nie istnieje w przeglądarce Firefox.
Christopher Chiche
@ChristopherChiche Nie wierzę, że to prawda. Jakiej wersji używasz? U mnie przynajmniej działa, a dokumentacja MDN wydaje się sugerować, że można założyć: developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model/ ...
couch i
1
Cóż, będę, to niewygodne. W każdym razie mój przykład był prawdopodobnie kiepski, ponieważ i tak musisz wyraźnie określić rozmiar svgelementu. AFAIK na wszystko, czego szukasz, biorąc pod uwagę dynamiczny rozmiar, na którym prawdopodobnie możesz polegać offsetWidth.
kanapa i
3
Dla każdego, kto przyjeżdża tutaj używając React 0.14 lub nowszego, .getDOMNode () nie jest już potrzebny: facebook.github.io/react/blog/2015/10/07/…
Hamund
2
Chociaż ta metoda może wydawać się najłatwiejszym (i najbardziej podobnym do jQuery) sposobem uzyskania dostępu do elementu, Facebook mówi teraz „Odradzamy to, ponieważ odwołania do ciągów znaków mają pewne problemy, są uważane za starsze i prawdopodobnie zostaną usunięte w przyszłości Jeśli obecnie używasz this.refs.textInput do uzyskiwania dostępu do referencji, zalecamy zamiast tego wzorzec wywołania zwrotnego ". Powinieneś użyć funkcji zwrotnej zamiast łańcucha znaków. Informacje tutaj
ahaurat
22

Alternatywnie do rozwiązania couchand możesz użyć findDOMNode

var Container = React.createComponent({

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

  render: function () {
    <svg />
  }
});
Łukasz Madon
źródło
10
Dla wyjaśnienia: w React <= 0.12.x użyj component.getDOMNode (), w React> = 0.13.x użyj React.findDOMNode ()
pxwise
2
@pxwise Aaaaa, a teraz w przypadku elementów DOM nie musisz nawet używać żadnej funkcji z React 0.14 :)
Andy
5
@pxwise Wierzę, że to ReactDOM.findDOMNode()teraz?
ivarni
1
@MichaelScheper To prawda, w moim kodzie jest trochę ES7. W mojej zaktualizowanej odpowiedzi react-measuredemonstracją jest (myślę) czysty ES6. Na pewno ciężko jest zacząć ... Przeszedłem przez to samo szaleństwo przez ostatnie półtora roku :)
Andy
2
@MichaelScheper przy okazji, możesz znaleźć przydatne wskazówki tutaj: github.com/gaearon/react-makes-you-sad
Andy
6

Mógłbyś skorzystać z biblioteki I, którą napisałem, która monitoruje rozmiar renderowanych komponentów i przekazuje je do Ciebie.

Na przykład:

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

Używa zoptymalizowanego algorytmu opartego na przewijaniu / obiektach, który pożyczyłem od ludzi znacznie sprytniejszych niż ja. :)

ctrlplusb
źródło
Świetnie, dzięki za udostępnienie. Miałem zamiar utworzyć repozytorium dla mojego niestandardowego rozwiązania również dla „DimensionProvidingHoC”. Ale teraz spróbuję.
larrydahooster
Byłbym wdzięczny za wszelkie opinie, pozytywne lub negatywne :-)
ctrlplusb
Dziękuję Ci za to. <Recharts /> wymaga ustawienia jawnej szerokości i wysokości, więc było to bardzo pomocne
JP DeVries