使用React重新调整浏览器的渲染视图


313

调整浏览器窗口大小时,如何让React重新渲染视图?

背景

我有一些块要在页面上单独布局,但是我还希望它们在浏览器窗口更改时更新。最终结果将类似于Ben Holland的 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
});

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

Answers:


530

使用React Hooks:

您可以定义一个自定义的Hook来监听window 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);
  }
}

5
这是如何运作的?该this.updateDimensions传给addEventListener只是一个光秃秃的函数引用这将对没有价值this时调用。是否应该使用匿名函数或.bind()调用来添加this,否则我是否会误解?
fadebee 2014年

24
@chrisdew我在这里有点晚了,但是React会自动绑定this直接在组件上定义的任何方法。
2014年

3
@MattDell是的,ES6类只是普通类,因此不能与它们自动绑定。
Michelle Tilley

30
没有jQuery的需要-使用innerHeightinnerWidthwindowcomponentWillMount如果getInitialState用于设置height和,则可以跳过width
sighrobot '16

2
@MattDell似乎::现在已经取消了绑定语法sitepoint.com/bind-javascripts-this-keyword-react “绑定操作符(::)不会成为ES7的一部分,因为ES7功能集已于2月冻结,bind运算符是ES8的建议”
Jarrod Smith,

130

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

仅当您为香草JS事件侦听器设置了与IE相关的polyfill时才有效
nnnn 2016年

@nnnn您能详细说明吗?我承认我只在Chrome浏览器中对此进行了测试。您是说window.addEventListener在没有polyfill的情况下无法在IE上工作吗?
安德烈·佩纳

2
@andrerpena caniuse.com/#search=addeventlistener表示ie8会有问题
nnnn

2
@nnnn。我知道了。是的。所以我的解决方案无法在IE 8上使用,但从9上的:)可以使用。谢谢。
安德烈·佩纳

35
真的有人还在乎IE8吗?还是只是习惯?
frostymarvelous

48

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

resize = () => this.forceUpdate()

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

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

12
别忘了限制力度更新,否则它看起来会非常小故障。
k2snowman69 '16

4
同样不要忘记删除监听器componentWillUnmount()
Jemar Jones

如果受到限制,这是最好的解决方案。我的意思是,如果forceUpdate有条件地应用
-asmmahmud

1
不是需要限制forceUpdate(称为事物),而是需要限制resize事件触发(事物触发)。从大小上将窗口调整大小时,从技术上讲,可以在每个像素处调用调整大小事件。当用户快速执行此操作时,发生的事件比您关心的更多。更糟糕的是,您将UI线程绑定到Javascript线程,这意味着您的应用在尝试分别处理每个事件时会开始变得非常缓慢。
k2snowman69

1
您不必在每个像素上都运行调整大小功能,而是在短时间内周期性地运行它,以产生流动感,您始终可以处理第一个和最后一个事件,从而感觉到它在流畅地处理调整大小。
k2snowman69

42

这是在没有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>
  );
};

4
这是一个很好的,简洁的答案,但是AFAICT有一个错误:除非我弄错了,否则::bind运算符每次应用都会返回一个新值。因此,您的事件监听器实际上不会取消注册,因为您removeEventListener最终将传递与原始传递给的函数不同的函数addEventListener
natevw

20

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

13

Edit 2018:现在React对上下文提供了一流的支持


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

如果您不关心副作用库,则可以使用Packery之类的方法

如果使用Flux,则可以创建一个包含窗口属性的存储,以便保留纯渲染功能,而不必每次都查询窗口对象。

在其他情况下,如果您想构建一个响应式网站,但更喜欢使用React内联样式而不是媒体查询,或者希望HTML / JS行为根据窗口宽度进行更改,请继续阅读以下内容:

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

React上下文不在公共API中,并允许将属性传递给组件的整个层次结构。

React上下文对于将永远不变的东西传递给整个应用程序特别有用(许多Flux框架通过mixin使用它)。您可以使用它来存储应用程序业务不变量(例如连接的userId,以便随处可见)。

但是它也可以用来存储可以改变的东西。问题在于,当上下文更改时,应该重新渲染使用该上下文的所有组件,而且这样做并不容易,最好的解决方案通常是使用新上下文卸载/重新安装整个应用程序。请记住,forceUpdate不是递归的

因此,如您所知,上下文是实用的,但是更改时会对性能产生影响,因此它不应经常更改。

要放在上下文中

  • 不变式:就像连接的userId,sessionToken一样,等等。
  • 事情不会经常改变

以下是一些不常更改的内容:

当前的用户语言

它不会经常更改,并且当它更改时,随着整个应用程序的翻译,我们必须重新呈现所有内容:一个很好的热语言更改用例

窗口属性

宽度和高度不会经常改变,但是当我们这样做时,我们的布局和行为可能不得不适应。对于布局,有时可以很容易地使用CSS媒体查询进行自定义,但有时却并非如此,并且需要不同的HTML结构。对于这种行为,您必须使用Javascript处理。

您不想在每个调整大小事件上重新渲染所有内容,因此您必须对调整大小事件进行去抖动。

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

例如:

  • 布局“ 1col”,宽度<= 600
  • 布局“ 2col”,用于600 <宽度<1000
  • 布局“ 3col”,宽度为1000 <=宽度

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

然后,您可以将布局类型与以前的布局类型进行比较,如果更改了布局类型,请使用新的上下文重新渲染应用程序:这可以避免在用户触发触发调整大小事件时完全重新渲染应用程序,但实际上布局类型未更改,因此仅在需要时才重新渲染。

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

如果使用Flux,则可以使用商店而不是React上下文,但是如果您的应用程序包含很多响应组件,使用上下文会更简单吗?


10

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

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

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

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

否则,您将得到警告:

警告:forceUpdate(...):只能更新已安装或正在安装的组件。这通常意味着您在未安装的组件上调用了forceUpdate()。这是无人值守。请检查XXX组件的代码。


1
您收到警告的原因是:您所做的是不当行为。您的render()函数应该是props和state的纯函数。在您的情况下,您应该在状态中保存新的大小。
zoran404

7

我将跳过以上所有答案,并开始使用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

1
这不提供窗口的大小,仅提供容器的大小。
mjtamlyn

3
是的,这就是重点。窗口的大小很难找到。容器的大小很难找到,并且对于React组件更有用。最新版本的react-dimensionsEven甚至可以通过编程方式更改尺寸(例如div的尺寸已更改,这会影响容器的尺寸,因此您需要更新尺寸)。Ben Alpert的答案仅对浏览器窗口大小调整事件有所帮助。看到这里:github.com/digidem/react-dimensions/issues/4
MindJuice '16

1
react-dimensions处理窗口大小的更改,flexbox布局的更改以及JavaScript调整大小引起的更改。我认为涵盖了这一点。您是否有一个示例,它不能正确处理?
MindJuice

2
窗口大小微不足道。无需图书馆:window.innerWidthwindow.innerHeightreact-dimensions解决了问题中更重要的部分,并且在调整窗口大小(以及更改容器大小时)时还触发了布局代码。
MindJuice

1
该项目不再得到积极维护。
Parabolord

7

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

6

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

这可能对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;
`

5

不知道这是否是最好的方法,但是对我有用的是首先创建一个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());
    }
}));

仅供参考,这些文件已部分修剪。


1
存储状态仅应响应已调度的操作而更改。绝对不是这样做的好方法。
frostymarvelous

2
实际上,@ frostymarvelous可能更好地重定位到了组件中,如其他答案中所述。
大卫·辛克莱

@DavidSinclair甚至更好,onresize可以调度一个WindowResizedAction
John Weisz

1
这太过分了。
詹森·赖斯

5

我知道已经回答了这个问题,但只是想与大家分享我的解决方案作为最佳答案,尽管不错,但现在可能有点过时了。

    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事件上反跳(仅每200ms运行一次)updateWindowDimensions以提高性能,但是在ComponentDidMount上调用它时不会反跳。

如果您经常安装防弹跳装置,我有时会发现防弹跳装置有时很难安装。

只是一个小的优化,但希望对您有所帮助!


initUpdateWindowDimensions方法的定义在哪里?
马特

它是在构造函数中定义的:)只是绑定到该组件的updateWindowDimensions,而没有去抖。
马特·威尔斯

3

只是为了改进@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>
    )
  }
}

2
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();
};

只需要定义事件大小调整功能。

然后更新渲染器大小(canvas),为相机分配新的宽高比。

在我看来,卸载和重新安装是一种疯狂的解决方案。

如果需要,下面是安装座。

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

2

想分享我刚刚发现使用的这个很酷的东西 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返回值,并使用它有条件地渲染组件,触发动作等。


1

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

1

谢谢大家的回答。这是我的React + Recompose。这是一个高级函数,其中包括组件的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');
 }})
)

1

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

0

因此,更好的方法是使用CSS或JSON文件数据中的数据,然后使用this.state({width:“ some value”,height:“ some value”});设置新状态。或编写代码,在自己的工作中使用宽屏数据的数据(如果您希望响应显示图像)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.