浏览器上的Rerender视图使用React调整大小

dig*_*ake 275 javascript resize reactjs

在调整浏览器窗口大小时,如何让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')
)
Run Code Online (Sandbox Code Playgroud)

然后我有了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>
    );
  }
});
Run Code Online (Sandbox Code Playgroud)

和列表/集合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>
    );
  }
});
Run Code Online (Sandbox Code Playgroud)

我应该添加jQuery的窗口调整大小吗?如果是的话,在哪里?

$( window ).resize(function() {
  // re-render the component
});
Run Code Online (Sandbox Code Playgroud)

有没有更"反应"的方式这样做?

Sop*_*ert 438

你可以在componentDidMount中监听,就像这个只显示窗口尺寸的组件一样(如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>;
}
Run Code Online (Sandbox Code Playgroud)

  • 不需要jQuery - 从`window`使用`innerHeight`和`innerWidth`.如果使用`getInitialState`来设置`height`和`width`,你可以跳过`componentWillMount`. (29认同)
  • @chrisdew我在这里有点晚了,但是React会自动绑定`this`以获取直接在组件上定义的任何方法. (23认同)
  • 这是如何运作的?传递给`addEventListener`的`this.updateDimensions`只是一个裸函数引用,在调用时它对`this`没有任何价值.应该使用匿名函数或.bind()调用来添加"this",还是我误解了? (5认同)
  • @MattDell是的,ES6类只是普通的类,所以没有自动绑定它们. (3认同)
  • 请参阅https://facebook.github.io/react/tips/dom-event-listeners.html :) (2认同)
  • @MattDell看起来像`::`bind语法现在已经出来https://www.sitepoint.com/bind-javascripts-this-keyword-react/"绑定运算符(::)不会成为ES7,因为ES7功能集在2月被冻结,绑定运算符是ES8的提议" (2认同)
  • 是否/应该为需要此逻辑的每个组件完成此操作? (2认同)

And*_*ena 128

@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);
    }
});
Run Code Online (Sandbox Code Playgroud)

  • 还有人真的关心IE8吗?还是只是习惯? (32认同)
  • @andrerpena http://caniuse.com/#search=addeventlistener表示ie8会有问题 (2认同)
  • @nnnn.我知道了.是..所以我的解决方案不适用于IE 8,但从9开始工作:).谢谢. (2认同)

sen*_*tor 48

非常简单的解决方案:

resize = () => this.forceUpdate()

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

componentWillUnmount() {
  window.removeEventListener('resize', this.resize)
}
Run Code Online (Sandbox Code Playgroud)

  • 不要忘记限制强制更新,否则它看起来会很奇怪. (12认同)
  • 另外不要忘记删除`componentWillUnmount()`上的监听器! (4认同)
  • 需要限制的不是forceUpdate(被调用的事物),而是需要限制的调整大小事件触发(触发的事物)。从技术上讲,当您将窗口大小从大调整为小时,可以在每个像素处调用调整大小事件。当用户快速执行此操作时,就会发生比您关心的更多事件。更糟糕的是,您将 UI 线程绑定到 Javascript 线程,这意味着您的应用程序在尝试单独处理每个事件时会开始感觉非常缓慢。 (2认同)
  • 您不是在每个像素上运行调整大小函数,而是在某个短的周期性时间量内运行它,以给人一种流动性的错觉,您总是处理第一个和最后一个事件,从而给人一种它正在流畅地处理调整大小的感觉。 (2认同)

voi*_*ice 34

这是使用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>
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的,简洁的答案,但AFAICT有一个错误:除非我弄错了,`::`bind运算符每次应用时都会返回一个新值.所以你的事件监听器实际上不会被取消注册,因为你的`removeEventListener`最终传递的函数不同于最初传递给`addEventListener`的函数. (3认同)

Lea*_*per 19

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);
  };
}
Run Code Online (Sandbox Code Playgroud)

像这样使用它。

function YourComponent() {
  const [width, height] = useWindowDimension();
  return <>Window width: {width}, Window height: {height}</>;
}
Run Code Online (Sandbox Code Playgroud)

  • 我使用同样的想法,仅在调整大小事件跨越 CSS 断点边界时“触发”钩子,即从移动大小调整为表格或桌面大小。 (2认同)

Ale*_*lex 17

从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}</>
}
Run Code Online (Sandbox Code Playgroud)


Seb*_*ber 13

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


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

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

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

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

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

React context an不在公共API中,并允许将属性传递给整个组件层次结构.

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

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

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

什么放在上下文中

  • 不变量:像连接的userId,sessionToken,无论什么......
  • 不经常改变的事情

以下是不经常更改的内容:

目前的用户语言:

它不会经常发生变化,当它发生变化时,整个应用程序都会被翻译,我们必须重新渲染所有内容:热门变换的一个非常好的用法

窗口属性

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

您不希望在每个resize事件上重新渲染所有内容,因此您必须去抖动调整大小事件.

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

例如:

  • 布局"1col",宽度<= 600
  • 布局"2col",600 <宽度<1000
  • 布局"3col",1000 <=宽度

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

然后,您可以将布局类型与以前的布局类型进行比较,如果已更改,则使用新的上下文重新呈现应用程序:这样可以避免在用户触发调整大小事件时实际重新呈现应用程序布局类型未更改,因此您只需在需要时重新呈现.

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

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


gkr*_*kri 10

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

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

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

handleResize = () => {
    this.forceUpdate();
};
Run Code Online (Sandbox Code Playgroud)

否则你会收到警告:

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


Min*_*ice 7

我将跳过所有上述答案并开始使用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
Run Code Online (Sandbox Code Playgroud)

  • 是的,这就是重点.窗口的大小很容易找到.容器的大小更难找到,对React组件更有用.最新版本的"react-dimensions"甚至适用于以编程方式更改的尺寸(例如,div的大小已更改,这会影响容器的大小,因此您需要更新大小).Ben Alpert的回答仅对浏览器窗口调整大小事件有帮助.请参见:https://github.com/digidem/react-dimensions/issues/4 (3认同)
  • 窗口大小是微不足道的.不需要一个库:`window.innerWidth`,`window.innerHeight`.`react-dimensions`解决了问题中更重要的部分,并在调整窗口大小时(以及容器大小更改时)触发布局代码. (2认同)
  • 该项目不再被积极维护。 (2认同)

Alb*_*ivé 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>;
    }
  }
Run Code Online (Sandbox Code Playgroud)

然后你可以在代码中的任何地方使用它,如下所示:

<WindowConsumer>
  {({ width, height }) =>  //do what you want}
</WindowConsumer>
Run Code Online (Sandbox Code Playgroud)


mpe*_*pen 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;
`
Run Code Online (Sandbox Code Playgroud)


Pat*_*sen 6

这个厨房里有很多厨师,但无论如何我都会投身其中。requestAnimationFrame我认为这些用途都不是性能最好的。

这是一个使用 React hooks 和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;
}
Run Code Online (Sandbox Code Playgroud)

这是显示其使用情况的要点:Img.tsx with useResize。或者,您可以在我的存储库中查看它获取更多上下文。

关于为什么应该这样做而不是消除函数反跳的一些资源:

感谢您参加我的 Ted 演讲。


Dav*_*air 5

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

然后我在我的组件中使用了那个商店,有点像这样:

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());
    }
}));
Run Code Online (Sandbox Code Playgroud)

仅供参考,这些文件被部分修剪.

  • @frostymarvelous确实可能更好地重新定位到组件中,就像在其他答案中一样. (2认同)

Mat*_*lls 5

I know this has been answered but just thought I'd share my solution as the top answer, although great, may now be a little outdated.

    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 })
    }
Run Code Online (Sandbox Code Playgroud)

The only difference really is that I'm debouncing (only running every 200ms) the updateWindowDimensions on the resize event to increase performance a bit, BUT not debouncing it when it's called on ComponentDidMount.

I was finding the debounce made it quite laggy to mount sometimes if you have a situation where it's mounting often.

Just a minor optimisation but hope it helps someone!


Ril*_*own 5

想分享我刚刚发现使用的这个非常酷的东西 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
    }
  };
Run Code Online (Sandbox Code Playgroud)

.matches 如果窗口高于或低于指定的 max-width 值,将返回 true 或 false,这意味着不需要限制侦听器,因为 matchMedia 仅在布尔值更改时触发一次。

我的代码可以很容易地调整为包含useState保存布尔 matchMedia 返回,并使用它来有条件地呈现组件、触发动作等。