反应“渲染后”代码?

时间:2014-10-24 21:19:36

标签: javascript reactjs

我有一个应用程序,我需要动态设置元素的高度(让我们说“app-content”)。它需要应用程序的“chrome”的高度并减去它,然后将“app-content”的高度设置为在这些约束内适合100%。使用vanilla JS,jQuery或Backbone视图这是非常简单的,但我很难弄清楚在React中执行此操作的正确流程是什么?

以下是一个示例组件。我希望能够将app-content的高度设置为窗口的100%减去ActionBarBalanceBar的大小,但我怎么知道所有内容的呈现时间以及我会把计算内容放在这个React类中吗?

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance={balance} />
          <div className="app-content">
            <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

module.exports = AppBase;

20 个答案:

答案 0 :(得分:253)

https://facebook.github.io/react/docs/react-component.html#componentdidmount

在渲染组件后调用此方法一次。所以你的代码看起来就是这样。

var AppBase = React.createClass({
  componentDidMount: function() {
    var $this = $(ReactDOM.findDOMNode(this));
    // set el height and width etc.
  },

  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
          <div className="inner-wrapper">
            <ActionBar title="Title Here" />
            <BalanceBar balance={balance} />
            <div className="app-content">
              <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

答案 1 :(得分:205)

使用componentDidUpdatecomponentDidMount的一个缺点是它们实际上是在完成dom元素绘制之前执行的,但是在它们从React传递到浏览器后#39 ; s DOM。

比如说你需要将node.scrollHeight设置为渲染的node.scrollTop,那么React的DOM元素可能还不够。你需要等到元素完成后才能达到它们的高度。

解决方案:

使用requestAnimationFrame确保在绘制新渲染的对象后运行代码

scrollElement: function() {
  //store a this ref, and
  var _this = this;
  //wait for a paint to do scrolly stuff
  window.requestAnimationFrame(function() {
    var node = _this.getDOMNode();
    if (node !== undefined) {
      //and scroll them!
      node.scrollTop = node.scrollHeight;
    }
  });
},
componentDidMount: function() {
  this.scrollElement();
},
// and or
componentDidUpdate: function() {
  this.scrollElement();
},
// and or
render: function() {
  this.scrollElement()
  return [...]

答案 2 :(得分:82)

根据我的经验window.requestAnimationFrame并不足以确保DOM已从componentDidMount完全呈现/重排完成。我运行的代码在componentDidMount调用后立即访问DOM,并且仅使用window.requestAnimationFrame将导致元素出现在DOM中;但是,由于尚未发生回流,因此对元素尺寸的更新尚未反映出来。

唯一真正可行的方法是将我的方法包装在setTimeoutwindow.requestAnimationFrame中,以确保React的当前调用堆栈在注册下一帧之前被清除&# 39; s渲染。

function onNextFrame(callback) {
    setTimeout(function () {
        window.requestAnimationFrame(callback)
    }, 0)
}

如果我不得不推测为什么会发生这种情况/必要,我可以看到React批处理DOM更新,而不是实际将更改应用到DOM,直到当前堆栈完成。

最终,如果您在代码中使用DOM测量,那么在React回调之后,您可能会想要使用此方法。

答案 3 :(得分:7)

React的生命周期方法很少有助于这些情况,列表包括但不限于 getInitialState,getDefaultProps,componentWillMount,componentDidMount 等。

在您的情况下以及需要与DOM元素交互的情况下,您需要等到dom准备就绪,因此请使用 componentDidMount ,如下所示:

/** @jsx React.DOM */
var List = require('../list');
var ActionBar = require('../action-bar');
var BalanceBar = require('../balance-bar');
var Sidebar = require('../sidebar');
var AppBase = React.createClass({
  componentDidMount: function() {
    ReactDOM.findDOMNode(this).height = /* whatever HEIGHT */;
  },
  render: function () {
    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar title="Title Here" />
          <BalanceBar balance={balance} />
          <div className="app-content">
            <List items={items} />
          </div>
        </div>
      </div>
    );
  }
});

module.exports = AppBase;

另外,有关生命周期反应的更多信息,请查看以下链接: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, componentWillMount, componentDidMount

答案 4 :(得分:5)

我觉得这个解决方案很脏,但我们走了:

componentDidMount() {
    this.componentDidUpdate()
}

componentDidUpdate() {
    // A whole lotta functions here, fired after every render.
}

现在我要坐在这里等待下来的选票。

答案 5 :(得分:5)

您可以更改状态,然后在setState callback中进行计算。根据React文档,这“保证在应用更新后会触发”。

这应该在componentDidMount或代码中的其他地方(例如在调整大小事件处理程序上)而不是在构造函数中完成。

这是window.requestAnimationFrame的一个很好的选择,它没有某些用户在这里提到的问题(需要将其与setTimeout结合使用或多次调用)。例如:

class AppBase extends React.Component {
    state = {
        showInProcess: false,
        size: null
    };

    componentDidMount() {
        this.setState({ showInProcess: true }, () => {
            this.setState({
                showInProcess: false,
                size: this.calculateSize()
            });
        });
    }

    render() {
        const appStyle = this.state.showInProcess ? { visibility: 'hidden' } : null;

        return (
            <div className="wrapper">
                ...
                <div className="app-content" style={appStyle}>
                    <List items={items} />
                </div>
                ...
            </div>
        );
    }
}

答案 6 :(得分:3)

实际上有比使用请求animationframe或timeout更简单,更简洁的版本。 Iam对没有人提出来感到惊讶: vanilla-js onload处理程序。 如果可以,请使用组件安装,否则,只需在jsx组件的onload处理程序上绑定一个函数。如果要让函数运行每个渲染,则在返回结果之前,还要执行它。代码如下:

runAfterRender = () => 
{
  const myElem = document.getElementById("myElem")
  if(myElem)
  {
    //do important stuff
  }
}

render()
{
  this.runAfterRender()
  return (
    <div
      onLoad = {this.runAfterRender}
    >
      //more stuff
    </div>
  )
}

}

答案 7 :(得分:3)

只需使用新的Hook方法来更新此问题,您只需使用useEffect钩子即可:

import React, { useEffect } from 'react'

export default function App(props) {

     useEffect(() => {
         // your post layout code (or 'effect') here.
         ...
     },
     // array of variables that can trigger an update if they change. Pass an
     // an empty array if you just want to run it once after component mounted. 
     [])
}

如果要在布局安装之前运行,请使用useLayoutEffect钩子:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

     useLayoutEffect(() => {
         // your pre layout code (or 'effect') here.
         ...
     }, [])
}

答案 8 :(得分:2)

渲染后,您可以像下面一样指定高度,并可以指定相应反应组件的高度。

render: function () {
    var style1 = {height: '100px'};
    var style2 = { height: '100px'};

   //window. height actually will get the height of the window.
   var hght = $(window).height();
   var style3 = {hght - (style1 + style2)} ;

    return (
      <div className="wrapper">
        <Sidebar />
        <div className="inner-wrapper">
          <ActionBar style={style1} title="Title Here" />
          <BalanceBar style={style2} balance={balance} />
          <div className="app-content" style={style3}>
            <List items={items} />
          </div>
        </div>
      </div>
    );`
  }

或者您可以使用sass指定每个反应组件的高度。指定前2个具有固定宽度的反应分量主分区,然后使用auto指定第三个分量主分区的高度。因此,根据第三个div的内容,将分配高度。

答案 9 :(得分:2)

我实际上遇到了类似行为的问题,我在一个Component中使用它的id属性渲染一个视频元素,所以当RenderDOM.render()结束时,它会加载一个插件,需要id来找到占位符并且它无法找到它。

componentDidMount()中的0ms的setTimeout修复了它:)

componentDidMount() {
    if (this.props.onDidMount instanceof Function) {
        setTimeout(() => {
            this.props.onDidMount();
        }, 0);
    }
}

答案 10 :(得分:1)

对我来说,window.requestAnimationFramesetTimeout的任何组合都不会产生一致的结果。有时它起作用,但并非总是 - 或者有时候为时已晚。

我根据需要多次循环window.requestAnimationFrame来修复它 (通常为0或2-3次)

关键是diff > 0:在这里,我们可以确切确定网页何时更新。

// Ensure new image was loaded before scrolling
if (oldH > 0 && images.length > prevState.images.length) {
    (function scroll() {
        const newH = ref.scrollHeight;
        const diff = newH - oldH;

        if (diff > 0) {
            const newPos = top + diff;
            window.scrollTo(0, newPos);
        } else {
            window.requestAnimationFrame(scroll);
        }
    }());
}

答案 11 :(得分:1)

我遇到了同样的问题。

my.fun<- function(band){ sprintf("%s<-data.frame(tetracam$filename,tetracam$time,tetracam$type,asd$%s,tetracam$%s)",band,band,band) sprintf("names(%s)<-c('filename','time','type','asd','tetracam')",band) sprintf("%s[order(%s$time),]",band,band) } 中使用hack-ish setTimeout(() => { }, 0)的大多数情况都有效。

但不是在特殊情况下;我并不想使用componentDidMount(),因为文档说:

  

注意:findDOMNode是用于访问底层DOM的转义符号   节点。在大多数情况下,不鼓励使用这种逃生舱,因为   它刺穿了组件抽象。

(资料来源:https://facebook.github.io/react/docs/react-dom.html#finddomnode

所以在那个特定组件中我必须使用ReachDOM findDOMNode事件,所以我的代码最终是这样的:

componentDidUpdate()

然后:

componentDidMount() {
    // feel this a little hacky? check this: http://stackoverflow.com/questions/26556436/react-after-render-code
    setTimeout(() => {
       window.addEventListener("resize", this.updateDimensions.bind(this));
       this.updateDimensions();
    }, 0);
}

最后,就我而言,我必须删除在componentDidUpdate() { this.updateDimensions(); } 中创建的侦听器:

componentDidMount

答案 12 :(得分:1)

使用ES6类而非React.createClass

进行一些更新
import React, { Component } from 'react';

class SomeComponent extends Component {
  constructor(props) {
    super(props);
    // this code might be called when there is no element avaliable in `document` yet (eg. initial render)
  }

  componentDidMount() {
    // this code will be always called when component is mounted in browser DOM ('after render')
  }

  render() {
    return (
      <div className="component">
        Some Content
      </div>
    );
  }
}

另外 - 检查React组件生命周期方法:https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle

每个组件都有很多类似于componentDidMount的方法,例如

  • componentWillUnmount() - 即将从浏览器DOM中删除组件

答案 13 :(得分:1)

来自ReactDOM.render()文档:

  

如果提供了可选的回调,它将在之后执行   组件被渲染或更新。

答案 14 :(得分:0)

当我需要打印接收大量数据并在画布上绘画的react组件时,我遇到了奇怪的情况。我已经尝试了所有提到的方法,但没有一种方法对我可靠地起作用,在setTimeout中使用requestAnimationFrame可以在20%的时间内获得空画布,所以我做了以下事情:

nRequest = n => range(0,n).reduce(
(acc,val) => () => requestAnimationFrame(acc), () => requestAnimationFrame(this.save)
);

基本上,我制作了一个requestAnimationFrame链,不确定是否是个好主意,但是到目前为止,这对我来说100%都有效(我使用30作为n变量的值)。

答案 15 :(得分:0)

我不会假装我知道为什么这个特定功能起作用,但是,当我需要使用 Ref <来访问DOM元素时, window.getComputedStyle 对我来说是100%的时间/ strong>中的 useEffect 中-我只能假定它也可以与 componentDidMount 一起使用。

我在 useEffect 中将其放在代码的顶部,并且出现,好像它迫使效果在继续绘制元素之前等待其被绘制下一行代码,但没有任何明显的延迟,例如使用 setTimeout 或异步睡眠功能。如果没有此设置,当我尝试访问Ref元素时,Ref元素将返回为 undefined

const ref = useRef(null);

useEffect(()=>{
    window.getComputedStyle(ref.current);
    // Next lines of code to get element and do something after getComputedStyle().
});

return(<div ref={ref}></div>);

答案 16 :(得分:0)

我目前正在使用钩子。
像这样:

import React, { useEffect } from 'react'


const AppBase = ({ }) => {

    useEffect(() => {
        // set el height and width etc.
    }, [])

    return (
        <div className="wrapper">
            <Sidebar />
            <div className="inner-wrapper">
                <ActionBar title="Title Here" />
                <BalanceBar balance={balance} />
                <div className="app-content">
                    <List items={items} />
                </div>
            </div>
        </div>
    );
}

export default AppBase

答案 17 :(得分:0)

对于功能组件,您可以react-use-call-onnext-render,它是一个自定义钩子,允许在以后的渲染中安排回调。

它在 one of my other projects 上成功使用。

要求 dom 元素的维度, 看这个例子,它是 react-use-call-onnext-render examples 上的第三个例子:

<块引用>

假设我们想要获取可移动 DOM 元素的尺寸,假设 divshowBox 状态控制 多变的。为此,我们可以使用 getBoundingClientRect()。然而,我们只想在元素之后调用这个函数 安装到 dom 中,因此将在负责显示此元素的变量之后安排此调用一次渲染 在dom中发生了变化,这个变量是showBox,所以他将成为useCallOnNextRender的依赖:

const YourComponent = () => {
    const [showBox, setShowBox] = useState(false)
    const divRef = useRef()
    const callOnNextShowBoxChange = useCallOnNextRender()
    return (
        <>
            <div style={canvasStyle} id="canvas">
                <button style={boxStyle} onClick={() => {
                    setShowBox(!showBox)
                    callOnNextShowBoxChange(() => console.log(divRef.current.getBoundingClientRect())) //right value
                }}>toggle show box
                </button>
                <div style={{border: "black solid 1px"}} ref={divRef}>
                    {showBox ? <div style={boxStyle}>box2</div> : null}
                </div>
            </div>
        </>
    );
};

答案 18 :(得分:0)

在尝试了上述所有建议的解决方案后,我发现中间的一个元素具有CSS 过渡,这就是为什么在道具更改后我未能获得正确的计算几何图形的原因。 所以我不得不使用 onTransitionEnd 侦听器等待片刻尝试获取容器元素的 DOM 高度计算。 希望这可以节省某人的工作日,哈哈。

答案 19 :(得分:0)

对我来说,单独的 componentDidUpdate 或单独的 window.requestAnimationFrame 没有解决问题,但以下代码有效。

// Worked but not succinct
    componentDidUpdate(prevProps, prevState, snapshot) {
        if (this.state.refreshFlag) {  // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
            window.requestAnimationFrame(() => {
                this.setState({
                    refreshFlag: false   // Set the refreshFlag back to false so this only runs once.
                });
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");  // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
            });
        }
    }

后来我测试了 requestAnimationFrame 评论,它仍然完美地工作:

// The best solution I found
    componentDidUpdate(prevProps, prevState, snapshot) {
        if (this.state.refreshFlag) {  // in the setState for which you want to do post-rendering stuffs, set this refreshFlag to true at the same time, to enable this block of code.
            // window.requestAnimationFrame(() => {
                this.setState({
                    refreshFlag: false   // Set the refreshFlag back to false so this only runs once.
                });
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");  // Do something that need to be done after rendering is finished. In my case I retrieved the canvas image.
            // });
        }
    }

我不确定额外的 setState 引起时间延迟是否只是巧合,因此在检索图像时,绘图已经完成(如果我删除了旧的画布图像,我将获得旧的画布图像setState).

或者更可能是因为setState需要在所有内容渲染完后才执行,所以强制等待渲染完成。

--我倾向于相信后者,因为根据我的经验,在我的代码中连续调用 setState 会导致每一个只在最后一次渲染完成后触发。

最后,我测试了以下代码。如果 this.setState({}); 不更新组件,而是等到渲染完成,这将是最终的最佳解决方案,我想。然而,它失败了。即使传递空的 {}setState() 仍会更新组件。

// This one failed!
    componentDidUpdate(prevProps, prevState, snapshot) {
        // if (this.state.refreshFlag) {
            // window.requestAnimationFrame(() => {
                this.setState({});
                something = this.scatterChart.current.canvas
                    .toDataURL("image/png");
            // });
        // }
    }