调整浏览器窗口大小时,如何让 React 重新渲染视图?
背景
我有一些想要在页面上单独布局的块,但是我也希望它们在浏览器窗口更改时更新。最终结果将类似于 Ben Holland's 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
组件(相当于上面 Pinterest 示例中的 Pin
):
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
});
有没有更“反应”的方式来做到这一点?
使用反应钩子:
您可以定义一个自定义 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>;
}
这里的好处是逻辑被封装了,你可以在任何你想使用窗口大小的地方使用这个Hook。
使用 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);
}
}
@SophieAlpert 是对的,+1,我只想提供她的解决方案的修改版本,without jQuery,基于 this answer。
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)
}
componentWillUnmount()
上的侦听器!
forceUpdate
这是一个不带 jQuery 使用 es6 的简单而简短的示例。
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>
);
};
::
绑定运算符在每次应用时都会返回一个新值。因此,您的事件侦听器实际上不会取消注册,因为您的 removeEventListener
最终传递的函数与最初传递给 addEventListener
的函数不同。
从 React 16.8 开始,您可以使用 Hooks!
/* 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}</>
}
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}</>;
}
2018 年编辑:现在 React 对 context 提供一流的支持
我将尝试给出一个通用的答案,针对这个特定的问题,但也是一个更普遍的问题。
如果您不关心副作用库,您可以简单地使用 Packery
如果您使用 Flux,您可以创建一个包含窗口属性的存储,这样您就可以保留一个纯渲染函数,而不必每次都查询窗口对象。
在其他情况下,如果您想构建一个响应式网站,但您更喜欢 React 内联样式而不是媒体查询,或者希望 HTML/JS 行为根据窗口宽度而变化,请继续阅读:
什么是 React 上下文以及我为什么要谈论它
React context 不在公共 API 中,允许将属性传递给整个组件层次结构。
React 上下文对于将永不改变的东西传递给整个应用程序特别有用(许多 Flux 框架通过 mixin 使用它)。您可以使用它来存储应用程序业务不变量(例如连接的 userId,以便它在任何地方都可用)。
但它也可以用来存储可以改变的东西。问题是当上下文发生变化时,所有使用它的组件都应该重新渲染,这样做并不容易,最好的解决方案通常是使用新的上下文卸载/重新安装整个应用程序。记住forceUpdate is not recursive。
正如您所理解的,上下文是实用的,但是当它改变时会对性能产生影响,所以它不应该经常改变。
在上下文中放置什么
不变量:比如连接的 userId、sessionToken 等等……
不经常改变的事情
以下是不经常改变的事情:
当前用户语言:
它不会经常变化,当它发生变化时,随着整个应用程序的翻译,我们必须重新渲染所有内容:一个非常好的热语言更改用例
窗口属性
宽度和高度不会经常变化,但是当我们这样做时,我们的布局和行为可能必须适应。对于布局,有时使用 CSS 媒体查询很容易自定义,但有时却不是,需要不同的 HTML 结构。对于您必须使用 Javascript 处理的行为。
您不想在每个调整大小事件上重新渲染所有内容,因此您必须对调整大小事件进行去抖动。
我对您的问题的理解是您想知道根据屏幕宽度要显示多少个项目。因此,您必须首先定义 responsive breakpoints,并列举您可以拥有的不同布局类型的数量。
例如:
布局“1col”,宽度 <= 600
布局“2col”,适用于 600 < 宽度 < 1000
布局“3col”,适用于 1000 <= 宽度
在调整大小事件(去抖动)时,您可以通过查询窗口对象轻松获取当前布局类型。
然后您可以将布局类型与之前的布局类型进行比较,如果它已更改,则使用新的上下文重新渲染应用程序:这允许在用户触发调整大小事件时完全避免重新渲染应用程序,但实际上布局类型没有改变,所以你只在需要时重新渲染。
一旦你有了它,你可以简单地在你的应用程序中使用布局类型(可以通过上下文访问),这样你就可以自定义 HTML、行为、CSS 类......你知道你在 React 渲染函数中的布局类型,所以这意味着你可以使用内联样式安全地编写响应式网站,并且根本不需要媒体查询。
如果你使用 Flux,你可以使用 store 代替 React 上下文,但是如果你的应用有很多响应式组件,使用上下文可能更简单?
我使用@senornestor 的解决方案,但要完全正确,您还必须删除事件侦听器:
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount(){
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.forceUpdate();
};
否则你会收到警告:
警告:forceUpdate(...):只能更新已安装或正在安装的组件。这通常意味着您在未安装的组件上调用了 forceUpdate()。这是一个无操作。请检查 XXX 组件的代码。
我会跳过以上所有答案并开始使用 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
react-dimensions
甚至适用于以编程方式更改的尺寸(例如,更改了 div 的大小,这会影响容器的大小,因此您需要更新大小)。 Ben Alpert 的回答仅有助于浏览器窗口调整大小事件。见这里:github.com/digidem/react-dimensions/issues/4
react-dimensions
处理窗口大小的变化、flexbox 布局的变化以及由于 JavaScript 调整大小而引起的变化。我认为这涵盖了它。你有一个不能正确处理的例子吗?
window.innerWidth
,window.innerHeight
。 react-dimensions
解决了问题中更重要的部分,并且在调整窗口大小时(以及容器大小发生变化时)也会触发您的布局代码。
此代码使用新的 React context API:
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>
不确定这是否是最好的方法,但对我有用的是首先创建一个 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());
}
}));
仅供参考,这些文件被部分修剪。
onresize
可以发送一个 WindowResizedAction
。
您不一定需要强制重新渲染。
这可能对 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;
`
想分享我刚刚使用 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,这意味着无需限制侦听器,因为 matchMedia 仅在布尔值更改时触发一次。
我的代码可以轻松调整为包含 useState
以保存布尔 matchMedia 返回,并使用它有条件地渲染组件、触发动作等。
我知道这已经得到解答,但只是想我会分享我的解决方案作为最佳答案,虽然很好,但现在可能有点过时了。
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 })
}
唯一的区别是我在调整大小事件上对 updateWindowDimensions 进行去抖动(仅每 200 毫秒运行一次)以提高性能,但在 ComponentDidMount 上调用它时不会去抖动。
我发现如果您遇到经常挂载的情况,去抖有时会使挂载变得相当滞后。
只是一个小的优化,但希望它可以帮助别人!
initUpdateWindowDimensions
方法的定义在哪里?
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();
};
只需要定义resize事件函数。
然后更新渲染器大小(画布),为相机分配一个新的纵横比。
在我看来,卸载和remouting是一个疯狂的解决方案......
如果需要,下面是安装。
<div
className={this.state.canvasActive ? 'canvasContainer isActive' : 'canvasContainer'}
ref={mount => {
this.mount = mount;
}}
/>
只是为了改进 @senornestor 的使用 forceUpdate
的解决方案和 @gkri 的解决方案以在组件卸载时删除 resize
事件侦听器:
不要忘记限制(或消除)调整大小的调用确保在构造函数中绑定(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>
)
}
}
必须在构造函数中将其绑定到“this”以使其使用 Class 语法
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)
}
}
谢谢大家的回答。这是我的反应 + Recompose。它是一个包含组件的 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" });如果您希望响应式显示图像,或者编写在自我工作中使用宽度屏幕数据数据的代码
从“反应”导入反应,{useState};类型 EventListener = () => void 让 eventListener: EventListener |不明确的;函数 setEventListener(updateSize: (size: number[]) => void){ if(eventListener){ window.removeEventListener('resize',eventListener); } eventListener = () => updateSize([window.innerWidth, window.innerHeight]);将事件侦听器作为事件侦听器返回; } function setResizer(updateSize: (size: number[]) => void) { window.addEventListener( 'resize', setEventListener(updateSize) ); } function useWindowSizeTableColumns() { const [size, setSize] = useState([ window.innerWidth || 0, window.innerHeight || 0 ]); setResizer(updateSize);返回大小;函数 updateSize(s: number[]) { if(size.some((v, i) => v !== s[i])){ setSize(s); } } } 导出默认useWindowSize;
在 index.js 中:
function render() {
ReactDOM.render(<App />, document.getElementById('root'));
}
render();
window.addEventListener('resize', render);
重新渲染强制重新计算依赖于 React 自身无法检测到的变量的所有内容,例如 window.innerWidth/innerHeight、localStorage 等,同时保持应用程序状态相同。
如果需要在其他情况下重新渲染,也可以导出这个 render() 函数并在其他地方使用。
我不确定这对性能的影响(因为它可能会重新渲染所有内容或只是改变了什么),但对我来说,调整大小时似乎足够快。
addEventListener
的this.updateDimensions
只是一个裸函数引用,在调用时对this
没有任何值。是否应该使用匿名函数或 .bind() 调用来添加this
,还是我误解了?this
用于直接在组件上定义的任何方法。window
中的innerHeight
和innerWidth
。如果使用getInitialState
设置height
和width
,则可以跳过componentWillMount
。::
绑定语法现在已经过时了 sitepoint.com/bind-javascripts-this-keyword-react “绑定运算符 (::) 不会成为 ES7 的一部分,因为 ES7 功能集在 2 月被冻结,绑定运算符是一个提议用于 ES8"