React的“after render”代码是什么?

478

我有一个应用程序,在其中需要动态设置元素高度(假设为“app-content”)。它获取应用程序的“chrome”高度并将其减去,然后设置“app-content”的高度以在这些限制条件内适合100%。使用原始的JS、jQuery或Backbone视图非常简单,但我无法确定在React中完成此操作的正确流程是什么?

以下是一个示例组件。我想要能够将app-content的高度设置为窗口高度减去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;

一个不用编程的替代方案是仅依赖于CSS,您可以使用flex、grow和height: 100vh的组合,以使元素始终完全覆盖屏幕大小。 - ed__
20个回答

350

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

240
如果值在第一次渲染后可以更改,那么请使用componentDidUpdate - zackify
6
我正在尝试更改一个设置为过渡的CSS属性,以便动画在渲染后开始。不幸的是,在componentDidMount()中更改CSS不会引起过渡。 - eye_mew
8
谢谢。这个名字非常直观,让我想知道为什么我还在尝试荒谬的名字,比如“init”或者甚至是“initialize”。 - Pawel
16
在componentDidMount中进行更改对浏览器来说太快了。将其包装在setTimeout中,并不实际地给它任何时间。例如:componentDidMount: () => { setTimeout(addClassFunction())},或者可以使用rAF(参见下面的答案)。 - user1596138
5
这绝对行不通。如果你获取了一个节点列表,然后尝试迭代该节点列表,你会发现长度等于0。我尝试做了一秒钟延时的 setTimeout,这个方法对我起作用了。不幸的是,React似乎没有一种真正等待DOM渲染完成的方法。 - NickJ
显示剩余4条评论

282

componentDidUpdatecomponentDidMount的一个缺点是它们实际上在DOM元素完成绘制前被执行,但在React将它们传递到浏览器的DOM后。

举个例子,如果您需要将node.scrollHeight设置为渲染节点的node.scrollTop,则React的DOM元素可能不足够。您需要等待元素完成绘制以获取其高度。

解决方法:

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

scrollElement: function() {
  // Store a 'this' ref, and
  var _this = this;
  // wait for a paint before running scrollHeight dependent code.
  window.requestAnimationFrame(function() {
    var node = _this.getDOMNode();
    if (node !== undefined) {
      node.scrollTop = node.scrollHeight;
    }
  });
},
componentDidMount: function() {
  this.scrollElement();
},
// and or
componentDidUpdate: function() {
  this.scrollElement();
},
// and or
render: function() {
  this.scrollElement()
  return [...]

41
window.requestAnimationFrame对我来说不够。我不得不用window.setTimeout进行修改。啊啊啊啊啊! - Alex
2
奇怪。也许在最新版本的React中已经改变了,我认为不需要调用requestAnimationFrame。文档说:“在组件更新被刷新到DOM后立即调用。此方法不会在初始渲染时调用。利用这个机会,在组件更新后操作DOM。”...也就是说,它被刷新后,DOM节点应该存在。-- https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate - Jim Soho
2
@JimSoho,我希望你是对的,即这个问题已经被解决了,但实际上文档中没有任何新信息。这是针对边缘情况的,当更新的DOM并不足够时,我们需要等待绘制周期非常重要。我试图使用新版本和旧版本创建一个演示该问题的复杂组件,但似乎无法做到,甚至回到了几个版本...... - Graham P Heath
4
严格来说,“[RAF]在下次重绘之前被称为[...]”-- [https://developer.mozilla.org/en-US/Apps/Fundamentals/Performance/CSS_Javascript_animation_performance#requestAnimationFrame]. 在这种情况下,节点仍需要通过DOM进行布局计算(即“回流”)。 这使用RAF作为一种从布局之前跳转到布局之后的方式。 Elm的浏览器文档是了解更多信息的好地方:http://elmprogramming.com/virtual-dom.html#how-browsers-render-html - Graham P Heath
3
这段代码是什么意思?"_this.getDOMNode is not a function" - OZZIE
显示剩余10条评论

123

根据我的经验,window.requestAnimationFrame无法确保DOM从componentDidMount完全渲染/回流完成。我有一段代码在componentDidMount调用后立即访问DOM,仅使用window.requestAnimationFrame将导致元素存在于DOM中;但是,由于尚未发生回流,因此尚未反映出元素尺寸的更新。

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

function onNextFrame(callback) {
    setTimeout(function () {
        requestAnimationFrame(callback)
    })
}

如果我不得不猜测为什么会发生这种情况/必要性,我可以看到React批处理DOM更新并且直到当前栈完成后才实际应用更改到DOM。

最终,如果您在React回调之后触发的代码中使用DOM度量,则可能需要使用此方法。


10
通常来说,你是正确的。但是在React的componentDidMount方法中,如果你在堆栈完成之前附加了一个requestAnimationFrame函数,DOM实际上可能并没有完全更新。我有一些代码可以在React的回调上下文中持续复现这种行为。为了确保你的代码执行(再次强调,针对这个特定的React使用情况),必须首先使用setTimeout清除调用堆栈,以确保DOM已经更新。 - Elliot Chong
6
请看上方其他评论,它们提到需要相同的解决方法,即:https://dev59.com/Ul8d5IYBdhLWcg3wsDxW#xq2gEYcBWogLw_1bvY3S这是唯一可靠的用于React应用场景的方法。如果我要猜测原因,可能是由于React自身批处理更新,这些更新可能不会在当前堆栈中应用(因此将requestAnimationFrame推迟到下一帧以确保批处理被应用)。 - Elliot Chong
4
我认为你可能需要加强对JS内部的了解... http://altitudelabs.com/blog/what-is-the-javascript-event-loop/ https://dev59.com/6Gsz5IYBdhLWcg3wNE5q - Elliot Chong
2
这个嵌套的 requestAnimationFrame 调用会更好吗?例如:function onNextFrame(cb) { window.requestAnimationFrame(_ => window.requestAnimationFrame(cb)) }。根据规范 (https://html.spec.whatwg.org/multipage/webappapis.html#animation-frames),这将保证在初始渲染后的下一帧运行(特别是,请查看“运行动画帧回调”的执行列表顺序)。它避免了 setTimeout 相对于下一帧何时执行的不确定性。 - Jess Telford
1
这种技术对我不起作用。就好像在回调函数被调用时DOM仍未布局一样。因为如果我将超时设置为1000进行快速测试,我会得到正确的尺寸。 - Jarrod Smith
显示剩余8条评论

43

仅更新一下这个问题,使用新的Hook方法,您可以简单地使用useEffect hook:

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.
         ...
     }, [])
}

1
根据React的文档,useLayoutEffect发生在所有DOM变化之后。https://reactjs.org/docs/hooks-reference.html#uselayouteffect - Philippe Hebert
3
没错,但它会在布局有机会绘制之前运行。 useLayoutEffect 内部安排的更新将被同步刷新,在浏览器有机会绘制之前进行处理。我会进行编辑。 - P Fuster
你是否知道 useEffect 是否在浏览器的回流之后运行(而不是 React 所谓的“绘制”)?使用 useEffect 请求元素的 scrollHeight 是安全的吗? - eMontielG
1
useEffect是安全的。 - P Fuster
是的,将我的组件从类重构为使用useEffect钩子函数的方式对我有用。 - orszaczky

21

您可以在 setState 回调函数 中更改状态并进行计算。根据 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>
        );
    }
}

14

我觉得这个解决方案很不好,但我们还是试试吧:

componentDidMount() {
    this.componentDidUpdate()
}

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

现在我只是坐在这里等待负评。


6
你应该尊重React组件的生命周期。 - Túbal Martín
3
我知道。如果你有更好的方法来达到相同的结果,随时分享。 - Jaakko Karhu
9
“Um, a figurative +1 for "sit here and wait for the down votes". Brave man. ;^)” 的意思是:“嗯,对于‘坐在这里等待被踩’的比喻,给一个点赞。勇敢的人。;^)” - ruffin
19
最好从两个生命周期中都调用一个方法,这样你就不必从其他生命周期触发循环了。 - Tjorriemorrie
2
componentWillReceiveProps 应该这样做 - Pablo
1
这太棒了。 - Yashdeep Hinge

12

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;

如果您想了解有关React生命周期的更多信息,可以查看下面的链接:https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState,getDefaultProps,componentWillMount,componentDidMount


我的组件在挂载前运行,导致页面渲染延迟很大,因为API调用需要加载数据。 - Jason G

9

我也遇到了同样的问题。

在大多数情况下,在 componentDidMount() 中使用 hack-ish 的 setTimeout(() => { }, 0) 方法可以解决问题。

但是在某些特殊情况下,我不想使用 ReachDOM findDOMNode,因为文档中指出:

注意:findDOMNode 是一个逃生口,用于访问底层 DOM 节点。在大多数情况下,不鼓励使用此逃生口,因为它突破了组件抽象。

(来源:findDOMNode

所以在那个特定的组件中,我必须使用 componentDidUpdate() 事件,所以我的代码最终变成了这样:

componentDidMount() {
    // feel this a little hacky? check this: https://dev59.com/Ul8d5IYBdhLWcg3wsDxW
    setTimeout(() => {
       window.addEventListener("resize", this.updateDimensions.bind(this));
       this.updateDimensions();
    }, 0);
}

然后:
componentDidUpdate() {
    this.updateDimensions();
}

最后,在我的情况下,我必须删除在componentDidMount中创建的监听器:
componentWillUnmount() {
    window.removeEventListener("resize", this.updateDimensions.bind(this));
}

6

实际上,有一个比使用requestAnimationFrame或timeout更简单、更清洁的版本。我很惊讶没有人提到过:使用vanilla-js onload处理程序。如果可以的话,请使用componentDidMount。如果不行,只需在jsx组件的onload处理程序上绑定一个函数。如果您希望该函数每次呈现时运行,则还需要在render函数返回结果之前执行它。代码如下:

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

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

}


太好了,谢谢!你的代码中有错别字吗?应该是 onLoad = {this.runAfterRender()},即调用函数。 - chichilatte
我认为你可以在render()函数的开头删除this.runAfterRender()调用。而且onLoad={this.runAfterRender}应该改为onLoad={this.runAfterRender()}。这样确实会在加载时触发该函数。 - Robert Cabri
这确实有效! - anlogg
在 React 17 中对我不起作用。 - Alex P.

4

我实际上遇到了类似的问题,我在一个组件中渲染了一个视频元素,并设置了它的id属性,所以当RenderDOM.render()结束时,它会加载一个需要id来找到占位符的插件,但是它找不到。

在componentDidMount()函数中加入0毫秒的setTimeout解决了这个问题 :)

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接