用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组件(相当于上面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
});

有没有更“React”的方式来做到这一点?

448400 次浏览

使用React钩子:

你可以定义一个自定义钩子来监听窗口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);
}
}

不确定这是否是最好的方法,但对我来说有效的方法是首先创建一个商店,我称之为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());
}
}));

供你参考,这些文件被部分删减了。

编辑2018:现在React有了对上下文的一级支持


我将尝试给出一个一般的答案,针对这个特定的问题,但也针对一个更普遍的问题。

如果你不关心副作用库,你可以简单地使用像Packery这样的东西

如果你使用Flux,你可以创建一个包含窗口属性的存储,这样你就可以保持一个纯粹的呈现函数,而不必每次都查询窗口对象。

在其他情况下,你想建立一个响应式网站,但你更喜欢React内联样式的媒体查询,或者希望HTML/JS行为根据窗口宽度改变,请继续阅读:

什么是React上下文?为什么我要谈论它

__abc0an不在公共API中,允许将属性传递给整个组件层次结构。

React上下文特别有用,它可以传递给你整个应用程序中永远不会改变的东西(许多Flux框架通过mixin使用它)。你可以用它来存储应用程序业务不变量(比如连接的userId,这样它就可以在任何地方使用)。

但它也可以用来存储可以改变的东西。问题是,当上下文改变时,所有使用它的组件都应该重新呈现,这并不容易做到,最好的解决方案通常是卸载/重新挂载整个应用程序与新的上下文。记得forceUpdate不是递归的

因此,正如您所理解的,上下文是实用的,但是当它改变时,会对性能产生影响,所以它不应该经常改变。

把什么放在上下文中

  • 不变量:比如连接的userId, sessionToken,等等…
  • 不经常改变的东西

以下是一些不会经常改变的东西:

当前用户语言:

它不会经常改变,当它改变时,整个应用程序都被翻译了,我们必须重新渲染所有内容:这是热语言变化的一个非常好的用例

窗口属性

宽度和高度不会经常改变,但当我们这样做时,我们的布局和行为可能不得不适应。对于布局,有时很容易使用CSS mediaqueries进行自定义,但有时不是,需要不同的HTML结构。对于行为,你必须用Javascript来处理。

你不希望在每个调整大小事件上重新渲染所有内容,所以你必须撤消调整大小事件。

我对你的问题的理解是,你想知道根据屏幕宽度显示多少项。所以你必须首先定义响应断点,并枚举你可以拥有的不同布局类型的数量。

例如:

  • 布局为“1col”,宽度<= 600
  • 布局“2col”,为600 <宽度& lt;1000
  • 布局“3col”,为1000 <=宽度

在调整大小事件(debpublished)上,您可以通过查询窗口对象轻松获得当前布局类型。

然后你可以比较布局类型与以前的布局类型,如果它已经改变了,重新呈现应用程序与一个新的上下文:这允许避免重新呈现应用程序在用户有触发调整大小事件,但实际上布局类型没有改变,所以你只在需要时重新呈现。

一旦你有了它,你就可以简单地在你的应用程序中使用布局类型(通过上下文访问),这样你就可以自定义HTML、行为、CSS类……你知道你的布局类型在React渲染函数中,所以这意味着你可以通过使用内联样式安全地编写响应式网站,而根本不需要介质查询。

如果你使用Flux,你可以使用一个商店而不是React上下文,但如果你的应用程序有很多响应组件,也许使用上下文更简单?

@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);
}
});

我将跳过上述所有答案,并开始使用react-dimensions高阶组件。

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

只需添加一个简单的import和一个函数调用,就可以在组件中访问this.props.containerWidththis.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

一个非常简单的解决方案:

resize = () => this.forceUpdate()


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


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

这是一个不使用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>
);
};

我使用@senornestor的解决方案,但要完全正确,你必须删除事件监听器以及:

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


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


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

否则你会得到警告:

警告:forceUpdate(…):只能更新已挂载或正在挂载 组件。这通常意味着在卸载的对象上调用forceUpdate() 组件。这是不可能的。请查一下XXX的代码 组件。< / p >

谢谢大家的回答。这是React + 重新安排。它是一个高阶函数,包含组件的windowHeightwindowWidth属性。

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');
}})
)

必须将它绑定到构造函数中的“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)
}
}

你不一定需要强制重新渲染。

这可能对OP没有帮助,但在我的情况下,我只需要更新画布上的widthheight属性(这是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;
`

我知道这个问题已经有人回答了,但我只是想把我的解决方案作为最重要的答案分享给大家,虽然很好,但现在可能有点过时了。

    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 })
}

唯一的区别是我在resize事件上deboundation(只运行每200ms一次)updateWindowDimensions来增加一点性能,但不是在ComponentDidMount上调用时deboundation它。

我发现,如果你经常安装它,那么反弹有时会使它的安装相当滞后。

只是一个小的优化,但希望它能帮助到别人!

由于这个原因,更好的是如果你使用这个数据来自CSS或JSON文件的数据,然后用这个数据设置新的状态。状态({width: "some value",height:"some value"});或者写代码谁使用宽屏幕数据的数据在自己的工作,如果你希望响应显示图像

这段代码使用了新的React上下文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>

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
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;
}}
/>

从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}</>
}

为了改进@senornestor使用forceUpdate的解决方案和@gkri在组件卸载时删除resize事件监听器的解决方案:

  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>
)
}
}

想要分享这个非常酷的东西,我刚刚发现使用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返回,并使用它有条件地呈现一个组件,火灾行动等。

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}</>;
}

import React, {useState} from 'react';


type EventListener = () => void
let eventListener: EventListener | undefined;


function setEventListener(updateSize: (size: number[]) => void){
if(eventListener){
window.removeEventListener('resize',eventListener);
}
eventListener = () => updateSize([window.innerWidth, window.innerHeight]);


return eventListener as EventListener;
}


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);


return size;


function updateSize(s: number[]) {
if(size.some((v, i) => v !== s[i])){
setSize(s);
}
}
}


export default useWindowSize;

试试这个:-

resize = () => this.forceUpdate()


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


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

在index.js:

function render() {
ReactDOM.render(<App />, document.getElementById('root'));
}


render();


window.addEventListener('resize', render);

重新渲染要求重新计算所有依赖于React本身无法检测到的变量的东西,比如window。innerWidth/innerHeight, localStorage等,同时保持应用程序状态不变。

如果需要在其他情况下重新渲染,也可以导出此render()函数并在其他地方使用。

虽然我不确定这对性能的影响(因为它可能会重新渲染所有内容或只是改变了什么),但对我来说,调整大小时它似乎足够快。

这个厨房里有很多厨师,但不管怎样我都要把我的帽子扔进去。这些都没有使用requestAnimationFrame,我认为这是最性能的。

下面是一个使用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。或者,你可以在我的repo 在这里中看到更多的上下文。

一些关于为什么你应该这样做而不是debouning你的函数的参考资料:

谢谢你们来听我的Ted演讲。

对于任何寻找打印稿支持的人来说,我已经根据@Lead开发人员回答写了下面的钩子:

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,它将不会反弹处理程序。< br > 另一个区别是钩子返回类型为Dimension的对象,即:

type Dimension = {
width: number;
height: number;
}

这样,每个人都清楚钩子实际返回的是什么。