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

背景

我有一些块,我想在页面上单独布局,但我也希望他们更新时,浏览器窗口的变化。最终的结果将是像Ben Holland的Pinterest布局,但使用React而不仅仅是jQuery。我还有一段路要走。

Code

这是我的应用程序:

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

和block的列表/集合:

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”的方式来做到这一点?


当前回答

这个厨房里有很多厨师,但不管怎样我都要把我的帽子扔进去。这些都没有使用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。或者,你可以在我的回购中看到更多的上下文。

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

一个相关的github公关与良好的解释 中等,但在一个迷因墙后面 一个深入的stackoverflow回答

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

其他回答

Edit 2018:现在React拥有对上下文的一流支持


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

如果您不关心副作用库,您可以简单地使用Packery之类的东西

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

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

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

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

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

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

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

把什么放在上下文中

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

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

当前用户语言:

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

窗口属性

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

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

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

例如:

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

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

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

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

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

在index.js:

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

render();

window.addEventListener('resize', render);

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

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

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

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

对于那些寻求Typescript支持的人,我根据@Lead 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支持,还有一个选项可以控制延迟时间(以毫秒为单位),默认为100ms。如果延迟时间为0或更少,它将不会反弹处理程序。 另一个区别是钩子返回一个Dimension类型的对象,即:

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

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

使用React钩子:

你可以定义一个自定义钩子来监听窗口大小调整事件,就像这样:

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>窗口大小: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);
  }
}