Rerender вид в браузере изменить размер с React

Как я могу заставить React повторно визуализировать представление при изменении размера окна браузера?

Фон

У меня есть несколько блоков, которые я хочу разместить на странице по отдельности, но я также хочу, чтобы они обновлялись при изменении окна браузера. Сам конечный результат будет похож на макет Pinterest Бена Холланда, но написан с использованием React, а не только jQuery. Я еще далеко.

Код

Вот мое приложение:

var MyApp = React.createClass({
  //does the http get from the server
  loadBlocksFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      mimeType: 'textPlain',
      success: function(data) {
        this.setState({data: data.events});
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentWillMount: function() {
    this.loadBlocksFromServer();

  },    
  render: function() {
    return (
        <div>
      <Blocks data={this.state.data}/>
      </div>
    );
  }
});

React.renderComponent(
  <MyApp url="url_here"/>,
  document.getElementById('view')
)

Тогда у меня есть компонент Block (эквивалентный Pin в приведенном выше примере Pinterest):

var Block = React.createClass({
  render: function() {
    return (
        <div class="dp-block" style={{left: this.props.top, top: this.props.left}}>
        <h2>{this.props.title}</h2>
        <p>{this.props.children}</p>
        </div>
    );
  }
});

и список/коллекция Blocks:

var Blocks = React.createClass({

  render: function() {

    //I've temporarily got code that assigns a random position
    //See inside the function below...

    var blockNodes = this.props.data.map(function (block) {   
      //temporary random position
      var topOffset = Math.random() * $(window).width() + 'px'; 
      var leftOffset = Math.random() * $(window).height() + 'px'; 
      return <Block order={block.id} title={block.summary} left={leftOffset} top={topOffset}>{block.description}</Block>;
    });

    return (
        <div>{blockNodes}</div>
    );
  }
});

Вопрос

Должен ли я добавить изменение размера окна jQuerys? Если да, то где?

$( window ).resize(function() {
  // re-render the component
});

Есть ли более "React" способ сделать это?

Ответ 1

Использование React Hooks:

Вы можете определить пользовательский Hook, который прослушивает событие окна resize, что-то вроде этого:

import React, { useLayoutEffect, useState } from 'react';

function useWindowSize() {
  const [size, setSize] = useState([0, 0]);
  useLayoutEffect(() => {
    function updateSize() {
      setSize([window.innerWidth, window.innerHeight]);
    }
    window.addEventListener('resize', updateSize);
    updateSize();
    return () => window.removeEventListener('resize', updateSize);
  }, []);
  return size;
}

function ShowWindowDimensions(props) {
  const [width, height] = useWindowSize();
  return <span>Window size: {width} x {height}</span>;
}

Преимущество здесь в том, что логика инкапсулирована, и вы можете использовать этот хук в любом месте, где хотите использовать размер окна.

Использование классов React:

Вы можете прослушивать в componentDidMount, что-то вроде этого компонента, который просто отображает размеры окна (например, <span>Window size: 1024 x 768</span>):

import React from 'react';

class ShowWindowDimensions extends React.Component {
  state = { width: 0, height: 0 };
  render() {
    return <span>Window size: {this.state.width} x {this.state.height}</span>;
  }
  updateDimensions = () => {
    this.setState({ width: window.innerWidth, height: window.innerHeight });
  };
  componentDidMount() {
    window.addEventListener('resize', this.updateDimensions);
  }
  componentWillUnmount() {
    window.removeEventListener('resize', this.updateDimensions);
  }
}

Ответ 2

@SophieAlpert прав, +1, я просто хочу предоставить модифицированную версию ее решения без jQuery, основываясь на этом ответе.

var WindowDimensions = React.createClass({
    render: function() {
        return <span>{this.state.width} x {this.state.height}</span>;
    },
    updateDimensions: function() {

    var w = window,
        d = document,
        documentElement = d.documentElement,
        body = d.getElementsByTagName('body')[0],
        width = w.innerWidth || documentElement.clientWidth || body.clientWidth,
        height = w.innerHeight|| documentElement.clientHeight|| body.clientHeight;

        this.setState({width: width, height: height});
        // if you are using ES2015 I'm pretty sure you can do this: this.setState({width, height});
    },
    componentWillMount: function() {
        this.updateDimensions();
    },
    componentDidMount: function() {
        window.addEventListener("resize", this.updateDimensions);
    },
    componentWillUnmount: function() {
        window.removeEventListener("resize", this.updateDimensions);
    }
});

Ответ 3

Очень простое решение:

resize = () => this.forceUpdate()

componentDidMount() {
  window.addEventListener('resize', this.resize)
}

componentWillUnmount() {
  window.removeEventListener('resize', this.resize)
}

Ответ 4

Это простой и короткий пример использования es6 без jQuery.

import React, { Component } from 'react';

export default class CreateContact extends Component {
  state = {
    windowHeight: undefined,
    windowWidth: undefined
  }

  handleResize = () => this.setState({
    windowHeight: window.innerHeight,
    windowWidth: window.innerWidth
  });

  componentDidMount() {
    this.handleResize();
    window.addEventListener('resize', this.handleResize)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize)
  }

  render() {
    return (
      <span>
        {this.state.windowWidth} x {this.state.windowHeight}
      </span>
    );
  }
}

крючки

import React, { useEffect, useState } from "react";

let App = () => {
  const [windowWidth, setWindowWidth] = useState(0);
  const [windowHeight, setWindowHeight] = useState(0);
  let resizeWindow = () => {
    setWindowWidth(window.innerWidth);
    setWindowHeight(window.innerHeight);
  };

  useEffect(() => {
    resizeWindow();
    window.addEventListener("resize", resizeWindow);
    return () => window.removeEventListener("resize", resizeWindow);
  }, []);

  return (
    <div>
      <span>
        {windowWidth} x {windowHeight}
      </span>
    </div>
  );
};

Ответ 5

Начиная с React 16.8, вы можете использовать крючки !

/* globals window */
import React, { useState, useEffect } from 'react'
import _debounce from 'lodash.debounce'

const Example = () => {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = _debounce(() => setWidth(window.innerWidth), 100)

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    }
  }, [])

  return <>Width: {width}</>
}

Ответ 6

Edit 2018: теперь React имеет первоклассную поддержку контекста


Я постараюсь дать общий ответ, который нацелен на эту конкретную проблему, но также и на более общую проблему.

Если вам не нужны побочные эффекты, вы можете просто использовать что-то вроде Packery

Если вы используете Flux, вы можете создать хранилище, содержащее свойства окна, чтобы вы сохраняли чистую функцию рендеринга без необходимости каждый раз запрашивать объект окна.

В других случаях, когда вы хотите создать адаптивный веб-сайт, но предпочитаете встроенные стили React медиазапросам или хотите, чтобы поведение HTML/JS изменялось в зависимости от ширины окна, продолжайте читать:

Что такое контекст React и почему я об этом говорю

Реагирующий контекст an не находится в общедоступном API и позволяет передавать свойства всей иерархии компонентов.

Контекст React особенно полезен для передачи всему вашему приложению того, что никогда не меняется (его используют многие фреймворки Flux через миксин). Вы можете использовать его для хранения бизнес-инвариантов приложения (например, подключенного userId, чтобы он был доступен везде).

Но его также можно использовать для хранения вещей, которые могут измениться. Проблема в том, что при изменении контекста все компоненты, которые его используют, должны быть перерисованы, и это нелегко сделать, лучшее решение часто состоит в том, чтобы размонтировать/перемонтировать все приложение с новым контекстом. Помните, что forceUpdate не является рекурсивным.

Итак, как вы понимаете, контекст практичен, но когда он меняется, это влияет на производительность, поэтому он не должен меняться слишком часто.

Что поставить в контекст

  • Инварианты: как подключенный userId, sessionToken, что угодно...
  • Вещи, которые не часто меняются

Вот вещи, которые не часто меняются:

Текущий язык пользователя:

Это не очень часто меняется, и когда это происходит, когда переводится все приложение, мы должны перерисовывать все: очень хороший пример использования горячих изменений языка.

Свойства окна

Ширина и высота меняются не часто, но когда мы делаем наш макет и поведение, возможно, придется адаптироваться. Иногда для макета его легко настроить с помощью медиазапросов CSS, но иногда это не так и требует другой структуры HTML. Для поведения вы должны справиться с этим с помощью Javascript.

Вы не хотите перерисовывать все при каждом событии изменения размера, поэтому вам нужно отменить изменения размера события.

Что я понимаю в вашей проблеме, так это то, что вы хотите знать, сколько элементов отображается в зависимости от ширины экрана. Таким образом, вы должны сначала определить отзывчивые точки останова и перечислить количество различных типов макетов, которые вы можете иметь.

Например:

  • Макет "1col", для ширины <= 600
  • Макет "2col", для 600 <ширина <1000
  • Макет "3col", для 1000 <= ширина

По событиям изменения размера (опровергнутым) вы можете легко получить текущий тип макета, запросив объект окна.

Затем вы можете сравнить тип макета с прежним типом макета и, если он изменился, повторно выполнить рендеринг приложения с новым контекстом: это позволяет вообще не повторять рендеринг приложения, когда пользователь запускает события изменения размера, но на самом деле Тип макета не изменился, поэтому вы можете выполнить повторную визуализацию только при необходимости.

Как только вы это сделаете, вы можете просто использовать тип макета внутри вашего приложения (доступный через контекст), чтобы вы могли настроить HTML, поведение, классы CSS... Вы знаете свой тип макета внутри функции рендеринга React, так что это означает, что вы может безопасно писать адаптивные веб-сайты, используя встроенные стили, и вообще не нуждается в медиазапросах.

Если вы используете Flux, вы можете использовать хранилище вместо контекста React, но если в вашем приложении много отзывчивых компонентов, может быть, проще использовать контекст?

Ответ 7

Я использую решение @senornestor, но, чтобы быть полностью правильным, вам нужно также удалить прослушиватель событий:

componentDidMount() {
    window.addEventListener('resize', this.handleResize);
}

componentWillUnmount(){
    window.removeEventListener('resize', this.handleResize);
}

handleResize = () => {
    this.forceUpdate();
};

В противном случае вы получите предупреждение:

Предупреждение: forceUpdate (...): может обновлять только установленный или монтируемый компонент. Обычно это означает, что вы вызвали forceUpdate() на unmounted компонент. Это не-op. Пожалуйста, проверьте код для XXX компонент.

Ответ 8

Я бы пропустил все приведенные выше ответы и начал бы использовать компонент высшего порядка react-dimensions.

https://github.com/digidem/react-dimensions

Просто добавьте простой import и вызов функции, и вы можете получить доступ к this.props.containerWidth и this.props.containerHeight в своем компоненте.

// Example using ES6 syntax
import React from 'react'
import Dimensions from 'react-dimensions'

class MyComponent extends React.Component {
  render() (
    <div
      containerWidth={this.props.containerWidth}
      containerHeight={this.props.containerHeight}
    >
    </div>
  )
}

export default Dimensions()(MyComponent) // Enhanced component

Ответ 9

Этот код использует новый API контекста React:

  import React, { PureComponent, createContext } from 'react';

  const { Provider, Consumer } = createContext({ width: 0, height: 0 });

  class WindowProvider extends PureComponent {
    state = this.getDimensions();

    componentDidMount() {
      window.addEventListener('resize', this.updateDimensions);
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.updateDimensions);
    }

    getDimensions() {
      const w = window;
      const d = document;
      const documentElement = d.documentElement;
      const body = d.getElementsByTagName('body')[0];
      const width = w.innerWidth || documentElement.clientWidth || body.clientWidth;
      const height = w.innerHeight || documentElement.clientHeight || body.clientHeight;

      return { width, height };
    }

    updateDimensions = () => {
      this.setState(this.getDimensions());
    };

    render() {
      return <Provider value={this.state}>{this.props.children}</Provider>;
    }
  }

Затем вы можете использовать его где угодно в своем коде, например так:

<WindowConsumer>
  {({ width, height }) =>  //do what you want}
</WindowConsumer>

Ответ 10

Вам необязательно принудительно повторять рендер.

Это может не помочь OP, но в моем случае мне нужно было обновить атрибуты width и height на моем холсте (которые вы не можете сделать с CSS).

Он выглядит следующим образом:

import React from 'react';
import styled from 'styled-components';
import {throttle} from 'lodash';

class Canvas extends React.Component {

    componentDidMount() {
        window.addEventListener('resize', this.resize);
        this.resize();
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this.resize);
    }

    resize = throttle(() => {
        this.canvas.width = this.canvas.parentNode.clientWidth;
        this.canvas.height = this.canvas.parentNode.clientHeight;
    },50)

    setRef = node => {
        this.canvas = node;
    }

    render() {
        return <canvas className={this.props.className} ref={this.setRef} />;
    }
}

export default styled(Canvas)`
   cursor: crosshair;
`

Ответ 11

Не уверен, что это лучший подход, но то, что сработало для меня, сначала создало Store, я назвал его WindowStore:

import {assign, events} from '../../libs';
import Dispatcher from '../dispatcher';
import Constants from '../constants';

let CHANGE_EVENT = 'change';
let defaults = () => {
    return {
        name: 'window',
        width: undefined,
        height: undefined,
        bps: {
            1: 400,
            2: 600,
            3: 800,
            4: 1000,
            5: 1200,
            6: 1400
        }
    };
};
let save = function(object, key, value) {
    // Save within storage
    if(object) {
        object[key] = value;
    }

    // Persist to local storage
    sessionStorage[storage.name] = JSON.stringify(storage);
};
let storage;

let Store = assign({}, events.EventEmitter.prototype, {
    addChangeListener: function(callback) {
        this.on(CHANGE_EVENT, callback);
        window.addEventListener('resize', () => {
            this.updateDimensions();
            this.emitChange();
        });
    },
    emitChange: function() {
        this.emit(CHANGE_EVENT);
    },
    get: function(keys) {
        let value = storage;

        for(let key in keys) {
            value = value[keys[key]];
        }

        return value;
    },
    initialize: function() {
        // Set defaults
        storage = defaults();
        save();
        this.updateDimensions();
    },
    removeChangeListener: function(callback) {
        this.removeListener(CHANGE_EVENT, callback);
        window.removeEventListener('resize', () => {
            this.updateDimensions();
            this.emitChange();
        });
    },
    updateDimensions: function() {
        storage.width =
            window.innerWidth ||
            document.documentElement.clientWidth ||
            document.body.clientWidth;
        storage.height =
            window.innerHeight ||
            document.documentElement.clientHeight ||
            document.body.clientHeight;
        save();
    }
});

export default Store;

Затем я использовал этот магазин в своих компонентах, вроде этого:

import WindowStore from '../stores/window';

let getState = () => {
    return {
        windowWidth: WindowStore.get(['width']),
        windowBps: WindowStore.get(['bps'])
    };
};

export default React.createClass(assign({}, base, {
    getInitialState: function() {
        WindowStore.initialize();

        return getState();
    },
    componentDidMount: function() {
        WindowStore.addChangeListener(this._onChange);
    },
    componentWillUnmount: function() {
        WindowStore.removeChangeListener(this._onChange);
    },
    render: function() {
        if(this.state.windowWidth < this.state.windowBps[2] - 1) {
            // do something
        }

        // return
        return something;
    },
    _onChange: function() {
        this.setState(getState());
    }
}));

FYI, эти файлы были частично обрезаны.

Ответ 12

Я знаю, что на этот вопрос уже был дан ответ, но подумал, что поделюсь своим решением, так как лучший ответ, хотя и отличный, может теперь немного устареть.

    constructor (props) {
      super(props)

      this.state = { width: '0', height: '0' }

      this.initUpdateWindowDimensions = this.updateWindowDimensions.bind(this)
      this.updateWindowDimensions = debounce(this.updateWindowDimensions.bind(this), 200)
    }

    componentDidMount () {
      this.initUpdateWindowDimensions()
      window.addEventListener('resize', this.updateWindowDimensions)
    }

    componentWillUnmount () {
      window.removeEventListener('resize', this.updateWindowDimensions)
    }

    updateWindowDimensions () {
      this.setState({ width: window.innerWidth, height: window.innerHeight })
    }

Единственное отличие на самом деле в том, что я отменяю (только через каждые 200 мс) updateWindowDimensions для события resize, чтобы немного повысить производительность, НО не отменяю его при вызове ComponentDidMount.

Я обнаружил, что отскок сделал его довольно запоздалым, если он часто монтируется.

Просто небольшая оптимизация, но надеюсь, что она кому-нибудь поможет!

Ответ 13

componentDidMount() {

    // Handle resize
    window.addEventListener('resize', this.handleResize);
}




handleResize = () => {
    this.renderer.setSize(this.mount.clientWidth, this.mount.clientHeight);
    this.camera.aspect = this.mount.clientWidth / this.mount.clientHeight;
    this.camera.updateProjectionMatrix();
};

Нужно только определить функцию изменения размера события.

Затем обновите размер рендера (холст), назначьте новое соотношение сторон для камеры.

Размонтирование и перенастройка - сумасшедшее решение, на мой взгляд....

ниже крепление при необходимости.

            <div
                className={this.state.canvasActive ? 'canvasContainer isActive' : 'canvasContainer'}
                ref={mount => {
                    this.mount = mount;
                }}
            />

Ответ 14

Просто чтобы улучшить решение @senornestor, использовать forceUpdate и решение @gkri для удаления прослушивателя события resize на компоненте unmount:

  1. не забудьте ограничить (или отменить) вызов для изменения размера
  2. убедитесь, что bind(this) в конструкторе
import React from 'react'
import { throttle } from 'lodash'

class Foo extends React.Component {
  constructor(props) {
    super(props)
    this.resize = throttle(this.resize.bind(this), 100)
  }

  resize = () => this.forceUpdate()

  componentDidMount() {
    window.addEventListener('resize', this.resize)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.resize)
  }

  render() {
    return (
      <div>{window.innerWidth} x {window.innerHeight}</div>
    )
  }
}

Другой метод - просто использовать "фиктивное" состояние вместо forceUpdate:

import React from 'react'
import { throttle } from 'lodash'

class Foo extends React.Component {
  constructor(props) {
    super(props)
    this.state = { foo: 1 }
    this.resize = throttle(this.resize.bind(this), 100)
  }

  resize = () => this.setState({ foo: 1 })

  componentDidMount() {
    window.addEventListener('resize', this.resize)
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.resize)
  }

  render() {
    return (
      <div>{window.innerWidth} x {window.innerHeight}</div>
    )
  }
}

Ответ 15

Спасибо всем за ответы. Здесь мой Реакт + Рекомендовать. Это функция высокого порядка, которая включает свойства windowHeight и windowWidth для компонента.

const withDimensions = compose(
 withStateHandlers(
 ({
   windowHeight,
   windowWidth
 }) => ({
   windowHeight: window.innerHeight,
   windowWidth: window.innerWidth
 }), {
  handleResize: () => () => ({
    windowHeight: window.innerHeight,
    windowWidth: window.innerWidth
  })
 }),
 lifecycle({
   componentDidMount() {
   window.addEventListener('resize', this.props.handleResize);
 },
 componentWillUnmount() {
  window.removeEventListener('resize');
 }})
)

Ответ 16

Привязать его к 'this' в конструкторе, чтобы заставить его работать с синтаксисом класса

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.resize = this.resize.bind(this)      
  }
  componentDidMount() {
    window.addEventListener('resize', this.resize)
  }
  componentWillUnmount() {
    window.removeEventListener('resize', this.resize)
  }
}

Ответ 17

https://github.com/renatorib/react-sizes - это HOC, чтобы сделать это, сохраняя при этом хорошую производительность.

import React from 'react'
import withSizes from 'react-sizes'

@withSizes(({ width }) => ({ isMobile: width < 480 }))
class MyComponent extends Component {
  render() {
    return <div>{this.props.isMobile ? 'Is Mobile' : 'Is Not Mobile'}</div>
  }
}

export default MyComponent

Ответ 18

Хотел поделиться этой довольно классной вещью, которую я только что нашел с помощью window.matchMedia

const mq = window.matchMedia('(max-width: 768px)');

  useEffect(() => {
    // initial check to toggle something on or off
    toggle();

    // returns true when window is <= 768px
    mq.addListener(toggle);

    // unmount cleanup handler
    return () => mq.removeListener(toggle);
  }, []);

  // toggle something based on matchMedia event
  const toggle = () => {
    if (mq.matches) {
      // do something here
    } else {
      // do something here
    }
  };

.matches вернет true или false, если окно выше или ниже указанного значения max-width, это означает, что нет необходимости регулировать слушателя, так как matchMedia запускается только один раз, когда изменяется логическое значение.

Мой код может быть легко скорректирован, чтобы включить useState для сохранения логических возвратов matchMedia и использовать его для условной визуализации компонента, действия при пожаре и т.д.

Ответ 19

По этой причине лучше, если вы используете эти данные из данных файла CSS или JSON, а затем с этими данными устанавливаете новое состояние с помощью this.state({width: "some value", height: "some value"}); или написание кода, который использует данные ширины экрана в самостоятельной работе, если вы хотите, чтобы отзывчивые изображения показа