当浏览器窗口调整大小时,如何让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”的方式来做到这一点?


当前回答

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

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

其他回答

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

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

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;

想分享这个很酷的东西,我用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
    }
  };

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

我的代码可以很容易地调整到包括useState来保存布尔matchMedia返回,并使用它有条件地呈现一个组件,火灾行动等。

这个厨房里有很多厨师,但不管怎样我都要把我的帽子扔进去。这些都没有使用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演讲。