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>
);
}
});
Вопрос
Должен ли я добавить изменение размера окна jQuery? Если так, то где?
$( window ).resize(function() {
// re-render the component
});
Есть ли более "Реактивный" способ сделать это?
25 ответов
Вы можете слушать в componentDidMount, что-то вроде этого компонента, который просто отображает размеры окна (например, <span>1024 x 768</span>
):
var WindowDimensions = React.createClass({
render: function() {
return <span>{this.state.width} x {this.state.height}</span>;
},
updateDimensions: function() {
this.setState({width: $(window).width(), height: $(window).height()});
},
componentWillMount: function() {
this.updateDimensions();
},
componentDidMount: function() {
window.addEventListener("resize", this.updateDimensions);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.updateDimensions);
}
});
@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);
}
});
Очень простое решение:
resize = () => this.forceUpdate()
componentDidMount() {
window.addEventListener('resize', this.resize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize)
}
Это простой и короткий пример использования 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>
);
}
}
Обновление в 2020 году. Для разработчиков React, серьезно относящихся к производительности.
Вышеупомянутые решения работают, НО будут повторно отображать ваши компоненты всякий раз, когда размер окна изменяется на один пиксель.
Это часто вызывает проблемы с производительностью, поэтому я написал useWindowDimension
крючок, который разрушает resize
событие на короткий период времени. например, 100 мс
import React, { useState, useEffect } from 'react';
export function useWindowDimension() {
const [dimension, setDimension] = useState([
window.innerWidth,
window.innerHeight,
]);
useEffect(() => {
const debouncedResizeHandler = debounce(() => {
console.log('***** debounced resize'); // See the cool difference in console
setDimension([window.innerWidth, window.innerHeight]);
}, 100); // 100ms
window.addEventListener('resize', debouncedResizeHandler);
return () => window.removeEventListener('resize', debouncedResizeHandler);
}, []); // Note this empty array. this effect should run only on mount and unmount
return dimension;
}
function debounce(fn, ms) {
let timer;
return _ => {
clearTimeout(timer);
timer = setTimeout(_ => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
Используйте это так.
function YourComponent() {
const [width, height] = useWindowDimension();
return <>Window width: {width}, Window height: {height}</>;
}
Начиная с 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}</>
}
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, но если в вашем приложении много отзывчивых компонентов, может быть, проще использовать контекст?
Я использую решение @senornestor, но чтобы быть полностью правильным, вы должны также удалить прослушиватель событий:
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount(){
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.forceUpdate();
};
В противном случае вы получите предупреждение:
Предупреждение: forceUpdate(...): может обновлять только смонтированный или монтируемый компонент. Обычно это означает, что вы вызвали forceUpdate() для несмонтированного компонента. Это неоперация. Пожалуйста, проверьте код для компонента XXX.
Хотел поделиться этой довольно классной вещью, которую я только что нашел, используя 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 и использовать их для условного рендеринга компонента, действия огня и т. д.
Я бы пропустил все приведенные выше ответы и начал бы использовать 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
Этот код использует новый 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() {
const { children } = this.props;
return <Provider value={this.state}>{children}</Provider>;
}
}
export default WindowProvider;
export const WindowConsumer = Consumer;
Затем вы можете использовать его где угодно в своем коде, например так:
<WindowConsumer>
{{(width, height)} => //do what you want}
</WindowConsumer>
Не уверен, что это лучший подход, но для меня сначала было создать Магазин, я назвал его 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());
}
}));
К вашему сведению, эти файлы были частично обрезаны.
Вам не обязательно форсировать повторную визуализацию.
Это может не помочь 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;
`
На этой кухне много поваров, но я все равно закину шляпу. Я бы сказал, что ни одно из этих применений не является наиболее эффективным.
Вот пример использования хуков React иrequestAnimationFrame
. Это также использует чистый js без каких-либо библиотек, таких как lodash (которых я избегаю любой ценой из-за размера пакета).
import { useState, useEffect, useCallback } from 'react';
const getSize = () => {
return {
width: window.innerWidth,
height: window.innerHeight,
};
};
export function useResize() {
const [size, setSize] = useState(getSize());
const handleResize = useCallback(() => {
let ticking = false;
if (!ticking) {
window.requestAnimationFrame(() => {
setSize(getSize());
ticking = false;
});
ticking = true;
}
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
Вот суть, показывающая его использование: Img.tsx с useResize . Кроме того, вы можете увидеть это в моем репо здесь для большего контекста.
Некоторые ресурсы о том, почему вы должны сделать это вместо устранения дребезга вашей функции:
- соответствующий github pr с хорошим объяснением
- средний, но за стеной мемов
- подробный ответ на stackoverflow
Спасибо, что пришли на мой Ted Talk.
Я знаю, что на этот вопрос уже был дан ответ, но подумал, что поделюсь своим решением, так как лучший ответ, хотя и отличный, может теперь немного устареть.
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.
Я обнаружил, что отскок иногда делает его довольно медленным, если вы часто его монтируете.
Просто небольшая оптимизация, но надеюсь, что она кому-нибудь поможет!
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;
}}
/>
Просто чтобы улучшить решение @senornestor для использования forceUpdate
и решение @gkri для удаления resize
прослушиватель событий для компонента unmount:
- не забудьте ограничить (или отменить) вызов для изменения размера
- убедитесь, что
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>
)
}
}
Пришлось связать его с этим в конструкторе, чтобы он работал с синтаксисом класса
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)
}
}
Спасибо всем за ответы. Вот мой Реакт + Рекомендовать. Это функция высокого порядка, которая включает в себя 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');
}})
)
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
Попробуй это :-
resize = () => this.forceUpdate()
componentDidMount() {
window.addEventListener('resize', this.resize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize)
}
По этой причине лучше, если вы используете эти данные из данных файла CSS или JSON, а затем с этими данными устанавливаете новое состояние с помощью this.state({width: "some value",height:"some value" }); или написание кода, который использует данные ширины экрана в самостоятельной работе, если вы хотите, чтобы изображения показывались быстро
В index.js:
function render() {
ReactDOM.render(<App />, document.getElementById('root'));
}
render();
window.addEventListener('resize', render);
Повторный рендеринг заставляет пересчитывать все, что зависит от переменных, которые React не обнаруживает сам по себе, таких как window.innerWidth/innerHeight, localStorage и т. д., сохраняя при этом состояние приложения прежним.
Если вам нужно выполнить повторный рендеринг в других ситуациях, вы также можете экспортировать эту функцию render() и использовать ее в других местах.
Я не уверен насчет влияния этого на производительность (поскольку это может быть повторный рендеринг всего или только того, что изменилось), но для меня это кажется достаточно быстрым при изменении размера.
Для тех, кто ищет поддержку Typescript , я написал следующий хук на основе user9234721 Developer : ответа
import { useState, useEffect } from 'react';
export const debounce = <A extends unknown[]>(callback: (...args: A) => unknown, msDelay: number) => {
let timer: NodeJS.Timeout | undefined;
return (...args: A) => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = undefined;
callback(...args);
}, msDelay);
};
};
export const useWindowDimension = (msDelay = 100) => {
const [dimension, setDimension] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const resizeHandler = () => {
setDimension({
width: window.innerWidth,
height: window.innerHeight
});
};
const handler = msDelay <= 0 ? resizeHandler : debounce(resizeHandler, msDelay);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
return dimension;
};
export type Dimension = ReturnType<typeof useWindowDimension>;
Приятной вещью, очевидно, является поддержка Typescript, а также возможность контролировать время задержки (вms
), который по умолчанию равен100ms
. Если время задержки0 or less
, он не будет устранять дребезг обработчика.
Еще одно отличие состоит в том, что хук возвращает объект типаDimension
, который:
type Dimension = {
width: number;
height: number;
}
Таким образом, всем понятно, что на самом деле возвращает хук.