Phản ứng lại sau khi render mã Code?


367

Tôi có một ứng dụng mà tôi cần đặt chiều cao của một phần tử (giả sử "nội dung ứng dụng") một cách linh hoạt. Nó lấy chiều cao của "chrome" của ứng dụng và trừ nó và sau đó đặt chiều cao của "nội dung ứng dụng" để phù hợp với 100% trong các ràng buộc đó. Điều này cực kỳ đơn giản với các khung nhìn vanilla JS, jQuery hoặc Backbone, nhưng tôi đang đấu tranh để tìm ra quy trình đúng sẽ là gì khi thực hiện điều này trong React?

Dưới đây là một thành phần ví dụ. Tôi muốn có thể đặt app-contentchiều cao của 100% cửa sổ trừ đi kích thước của ActionBarBalanceBar, nhưng làm thế nào để biết khi nào mọi thứ được hiển thị và tôi sẽ đặt công cụ tính toán vào Lớp Phản ứng này ở đâu?

/** @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;

4
Lúc đầu, tôi giống như "flexbox sẽ khắc phục điều này như thế nào?" sau đó tôi nhớ tính năng cột trong Flexbox và nó hoạt động như một bùa mê!
Oscar Godson

Câu trả lời:


301

thành phầnDidMount ()

Phương thức này được gọi một lần sau khi thành phần của bạn được hiển thị. Vì vậy, mã của bạn sẽ trông như vậy.

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

214
hoặc componentDidUpdatenếu các giá trị có thể thay đổi sau lần kết xuất đầu tiên.
zackify

5
Tôi đang cố gắng thay đổi thuộc tính css được đặt thành chuyển đổi, để hoạt ảnh bắt đầu sau khi kết xuất. Thật không may, thay đổi css trong componentDidMount()không gây ra sự chuyển đổi.
eye_mew

8
Cảm ơn. Cái tên rất trực quan đến nỗi tôi tự hỏi tại sao tôi lại thử những cái tên lố bịch như "init" hay thậm chí là "khởi tạo".
Pawel

13
Thay đổi nó trong thành phầnDidMount là quá nhanh đối với trình duyệt. Gói nó trong setTimeout và không có thời gian thực tế. tức là componentDidMount: () => { setTimeout(addClassFunction())}, hoặc sử dụng rAF, câu trả lời dưới đây cung cấp câu trả lời này.

3
Điều này chắc chắn KHÔNG làm việc. Nếu bạn nhận được một danh sách nút và sau đó thử lặp lại danh sách nút, bạn sẽ thấy độ dài bằng 0. Thực hiện setTimeout và chờ 1 giây làm việc cho tôi. Thật không may, nó không xuất hiện phản ứng có một phương thức thực sự chờ đợi cho đến khi DOM được hiển thị.
NickJ

241

Một nhược điểm của việc sử dụng componentDidUpdate, hoặccomponentDidMount là chúng thực sự được thực thi trước khi các phần tử dom được rút ra, nhưng sau khi chúng được chuyển từ React sang DOM của trình duyệt.

Nói ví dụ nếu bạn cần đặt node.scrollHeight cho nút được kết xuất.scrollTop, thì các phần tử DOM của React có thể không đủ. Bạn cần đợi cho đến khi các yếu tố được vẽ xong để có được chiều cao của chúng.

Giải pháp:

Sử dụng requestAnimationFrameđể đảm bảo rằng mã của bạn được chạy sau khi vẽ đối tượng vừa được kết xuất của bạn

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 [...]

29
window.requestAnimationFrame là không đủ cho tôi. Tôi đã phải hack nó với window.setTimeout. Argggghhhhhh !!!!!
Alex

2
Lạ Có lẽ nó đã thay đổi trong phiên bản React gần đây nhất, tôi không nghĩ rằng lệnh gọi requestAnimationFrame là cần thiết. Tài liệu nói: "Được gọi ngay sau khi các bản cập nhật của thành phần được chuyển sang DOM. Phương thức này không được gọi cho kết xuất ban đầu. Sử dụng điều này như một cơ hội để hoạt động trên DOM khi thành phần đã được cập nhật." ... nó bị xóa, nút DOM phải có mặt. - facebook.github.io/react/docs/ khăn
Jim Soho

1
@JimSoho, tôi hy vọng bạn đúng rằng điều này đã được sửa, nhưng thực sự không có gì mới trong tài liệu đó. Điều này là cho các trường hợp cạnh mà dom được cập nhật là không đủ, và điều quan trọng là chúng tôi chờ đợi chu kỳ sơn. Tôi đã cố gắng tạo ra một câu đố với các phiên bản mới và cũ, nhưng dường như tôi không thể tạo ra một thành phần đủ phức tạp để chứng minh vấn đề, thậm chí quay lại một vài phiên bản ...
Graham P Heath

3
@neptunian Nói một cách nghiêm túc "[RAF] được gọi là [...] trước lần tái bản tiếp theo ..." - [ developer.mozilla.org/en-US/Apps/Fundamentals/Performance/, ]. Trong tình huống này, nút vẫn cần phải được bố trí bởi DOM (hay còn gọi là "được chỉnh lại"). Điều này sử dụng RAF như một cách để nhảy từ trước khi bố trí sang sau khi bố trí. Tài liệu trình duyệt từ Elm là một nơi tốt để biết thêm: elmprogramming.com/virtual-dom.html#how-browsers-render-html
Graham P Heath

1
_this.getDOMNode is not a functionmã này là cái quái gì vậy?
OZZIE

104

Theo kinh nghiệm của tôi window.requestAnimationFramelà không đủ để đảm bảo rằng DOM đã được hoàn trả / hoàn nguyên từ hoàn chỉnh componentDidMount. Tôi có mã đang chạy truy cập DOM ngay sau khi componentDidMountgọi và chỉ sử dụngwindow.requestAnimationFrame sẽ dẫn đến yếu tố có mặt trong DOM; tuy nhiên, các cập nhật cho kích thước của phần tử chưa được phản ánh vì chưa xảy ra hiện tượng trào ngược.

Cách duy nhất thực sự đáng tin cậy để làm việc này là bọc phương thức của tôi trong setTimeoutwindow.requestAnimationFrameđể đảm bảo ngăn xếp cuộc gọi hiện tại của React bị xóa trước khi đăng ký kết xuất khung hình tiếp theo.

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

Nếu tôi phải suy đoán lý do tại sao điều này xảy ra / cần thiết, tôi có thể thấy các bản cập nhật DOM theo đợt của React và không thực sự áp dụng các thay đổi cho DOM cho đến khi ngăn xếp hiện tại hoàn tất.

Cuối cùng, nếu bạn đang sử dụng các phép đo DOM trong mã bạn đang kích hoạt sau các cuộc gọi lại React, có lẽ bạn sẽ muốn sử dụng phương pháp này.


1
Bạn chỉ cần setTimeout HOẶC requestAnimationFrame, không phải cả hai.

7
Thông thường- bạn đúng. Tuy nhiên, trong ngữ cảnh của phương thức thành phầnDidMount của React nếu bạn đính kèm requestAnimationFrame trước khi ngăn xếp đó kết thúc, DOM có thể không thực sự được cập nhật đầy đủ. Tôi có mã luôn tái tạo hành vi này trong bối cảnh các cuộc gọi lại của React. Cách duy nhất để chắc chắn rằng mã của bạn đang thực thi (một lần nữa, trong trường hợp sử dụng React cụ thể này) sau khi DOM được cập nhật là để ngăn xếp cuộc gọi rõ ràng trước với setTimeout.
Elliot Chong

6
Bạn sẽ nhận thấy các ý kiến ​​khác ở trên có đề cập đến việc cần cùng một cách giải quyết, ví dụ: stackoverflow.com/questions/26556436/react-after-render-code/ trộm Đây là phương pháp đáng tin cậy duy nhất 100% cho trường hợp sử dụng React này. Nếu tôi phải mạo hiểm đoán thì có thể là do các bản cập nhật theo đợt React có khả năng không được áp dụng trong ngăn xếp hiện tại (do đó trì hoãn requestAnimationFrame cho khung tiếp theo để đảm bảo lô được áp dụng).
Elliot Chong

4
Tôi nghĩ rằng bạn có thể cần phải kiểm tra nội bộ JS của mình ... altitudelabs.com/blog/what-is-the-javascript-event-loop stackoverflow.com/questions/8058612/ trộm
Elliot Chong

2
Chờ đợi ngăn xếp cuộc gọi hiện tại rõ ràng không phải là một cách "vô nghĩa" để nói về vòng lặp sự kiện, nhưng với mỗi cuộc phỏng vấn của riêng tôi, tôi đoán ...
Elliot Chong

17

Chỉ cần cập nhật một chút câu hỏi này với các phương thức Hook mới, bạn chỉ cần sử dụng useEffecthook:

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

Ngoài ra nếu bạn muốn chạy trước khi sơn bố trí, hãy sử dụng useLayoutEffecthook:

import React, { useLayoutEffect } from 'react'

export default function App(props) {

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

Theo tài liệu của React, useLayoutEffect xảy ra sau khi tất cả các đột biến DOM Reacjs.org/docs/hooks-reference.html#uselayouteffect
Philippe Hebert

Đúng, nhưng nó chạy trước khi bố cục có cơ hội vẽ Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.tôi sẽ chỉnh sửa.
P Fuster

Bạn có tình cờ biết nếu useEffect chạy sau phản xạ của trình duyệt (không phải là những gì React gọi là 'paint') không? Có an toàn khi yêu cầu cuộn phần tử với độ cao của useEffect không?
eMontielG

Nó an toàn để sử dụngEffect
P Fuster

13

Bạn có thể thay đổi trạng thái và sau đó thực hiện các tính toán của mình trong cuộc gọi lại setState . Theo tài liệu React, điều này "được đảm bảo để kích hoạt sau khi cập nhật được áp dụng".

Điều này nên được thực hiện trong componentDidMount hoặc một nơi nào khác trong mã (như trên trình xử lý sự kiện thay đổi kích thước) chứ không phải trong hàm tạo.

Đây là một giải pháp thay thế tốt window.requestAnimationFramevà nó không có vấn đề mà một số người dùng đã đề cập ở đây (cần kết hợp với nó setTimeouthoặc gọi nó nhiều lần). Ví dụ:

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

1
Đây là câu trả lời yêu thích của tôi. Mã React sạch và tốt.
phatmann

1
Đây là một câu trả lời tuyệt vời! Cảm ơn!
Bryan Jyh Herng Chong

1
Thật là đẹp, upvote bình luận này!
bingeScripter

11

Tôi cảm thấy rằng giải pháp này là bẩn, nhưng ở đây chúng tôi đi:

componentDidMount() {
    this.componentDidUpdate()
}

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

Bây giờ tôi chỉ ngồi đây và chờ số phiếu giảm.


5
Bạn nên tôn trọng vòng đời thành phần React.
Túbal Martín

2
@ TúbalMartín Tôi biết. Nếu bạn có cách tốt hơn để đạt được kết quả tương tự, hãy thoải mái chia sẻ.
Jaakko Karhu

8
Ừm, một nghĩa bóng +1 cho "ngồi đây và chờ số phiếu giảm". Người đàn ông dũng cảm. ; ^)
ruffin

14
Thay vì gọi một phương thức từ cả hai vòng đời, thì bạn không phải kích hoạt các chu kỳ từ các chu kỳ khác.
Tjorriemorrie

1
thành phầnWillReceiveProps nên làm điều này
Pablo

8

React có một vài phương thức vòng đời giúp ích trong những tình huống này, các danh sách bao gồm nhưng không giới hạn ở getInitialState, getDefaultProps, componentWillMount, componentDidMount, v.v.

Trong trường hợp của bạn và các trường hợp cần tương tác với các phần tử DOM, bạn cần đợi cho đến khi dom sẵn sàng, vì vậy hãy sử dụng thành phầnDidMount như dưới đây:

/** @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;

Ngoài ra để biết thêm thông tin về vòng đời trong phản ứng, bạn có thể xem liên kết dưới đây: https://facebook.github.io/react/docs/state-and-lifecycle.html

getInitialState, getDefaultProps, thành phầnWillMount, thành phầnDidMount


thành phần của tôi đã chạy mount trước khi trang hiển thị gây ra độ trễ lớn khi một cuộc gọi api tải dữ liệu.
Jason G

6

Tôi gặp vấn đề tương tự.

Trong hầu hết các tình huống sử dụng hack-ish setTimeout(() => { }, 0)trong componentDidMount()làm việc.

Nhưng không phải trong trường hợp đặc biệt; và tôi không muốn sử dụng ReachDOM findDOMNodevì tài liệu nói:

Lưu ý: findDOMNode là một lối thoát được sử dụng để truy cập nút DOM bên dưới. Trong hầu hết các trường hợp, việc sử dụng lối thoát này không được khuyến khích vì nó xuyên qua sự trừu tượng thành phần.

(Nguồn: findDOMNode )

Vì vậy, trong thành phần cụ thể đó, tôi đã phải sử dụng componentDidUpdate()sự kiện này, vì vậy mã của tôi đã kết thúc như thế này:

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

Và sau đó:

componentDidUpdate() {
    this.updateDimensions();
}

Cuối cùng, trong trường hợp của tôi, tôi đã phải loại bỏ trình nghe được tạo trong componentDidMount:

componentWillUnmount() {
    window.removeEventListener("resize", this.updateDimensions.bind(this));
}

2

Sau khi kết xuất, bạn có thể chỉ định chiều cao như bên dưới và có thể chỉ định chiều cao cho các thành phần phản ứng tương ứng.

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

hoặc bạn có thể chỉ định chiều cao của từng thành phần phản ứng bằng cách sử dụng sass. Chỉ định 2 div chính của thành phần phản ứng đầu tiên với chiều rộng cố định và sau đó là chiều cao của div chính của thành phần thứ ba với auto. Vì vậy, dựa trên nội dung của div thứ ba, chiều cao sẽ được chỉ định.


2

Tôi thực sự gặp rắc rối với hành vi tương tự, tôi kết xuất một thành phần video trong Thành phần có thuộc tính id để khi RenderDOM.render () kết thúc, nó tải một plugin cần id để tìm trình giữ chỗ và không tìm thấy nó.

SetTimeout với 0ms bên trong thành phầnDidMount () đã sửa nó :)

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

1

Từ tài liệu ReactDOM.render () :

Nếu gọi lại tùy chọn được cung cấp, nó sẽ được thực hiện sau khi thành phần được kết xuất hoặc cập nhật.


9
bạn có thể thêm một ví dụ về cách sử dụng này? Tôi chủ yếu trả về các phần tử từ phương thức kết xuất, tôi không gọi kết xuất và cung cấp các giá trị.
dcsan

23
Thật không may là callback bạn đề cập đến là chỉ có sẵn trong cho các mục cấp đầuReactDOM.render , chứ không phải cho các thành phần cấp độ củaReactElement.render (đó là đối tượng ở đây).
Bramus

1
Ví dụ ở đây sẽ hữu ích
DanV

1
Tôi đã nhấp vào liên kết trong câu trả lời của bạn và tôi không thể tìm thấy dòng bạn trích dẫn và câu trả lời của bạn không bao gồm đủ thông tin để làm việc mà không có nó. Vui lòng xem stackoverflow.com/help/how-to-answer để được tư vấn về cách viết một câu hỏi hay
Benubird

1

Đối với tôi, không có sự kết hợp window.requestAnimationFramehoặc setTimeouttạo ra kết quả phù hợp. Đôi khi nó hoạt động, nhưng không phải lúc nào cũng hay, đôi khi sẽ là quá muộn.

Tôi đã sửa nó bằng cách lặp window.requestAnimationFramenhiều lần nếu cần.
(Thường là 0 hoặc 2-3 lần)

Chìa khóa là diff > 0: ở đây chúng tôi có thể đảm bảo chính xác khi trang cập nhật.

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

0

Tôi đã có một tình huống kỳ lạ khi tôi cần in thành phần phản ứng nhận được lượng lớn dữ liệu và vẽ trên vải. Tôi đã thử tất cả các cách tiếp cận được đề cập, không phải cách nào trong số chúng hoạt động đáng tin cậy đối với tôi, với requestAnimationFrame bên trong setTimeout tôi nhận được canvas trống trong 20% ​​thời gian, vì vậy tôi đã làm như sau:

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

Về cơ bản tôi đã tạo ra một chuỗi requestAnimationFrame, không chắc đây có phải là ý tưởng tốt hay không nhưng điều này hoạt động trong 100% các trường hợp đối với tôi cho đến nay (tôi đang sử dụng 30 làm giá trị cho biến n).


0

Thực sự có một phiên bản đơn giản và gọn gàng hơn nhiều so với việc sử dụng yêu cầu hoạt hình hoặc thời gian chờ. Iam ngạc nhiên không ai đưa nó lên: trình xử lý tải vanilla-js. Nếu bạn có thể, hãy sử dụng thành phần đã gắn kết, nếu không, chỉ cần liên kết một hàm trên hanlder onload của thành phần jsx. Nếu bạn muốn chức năng chạy mọi kết xuất, hãy thực hiện nó trước khi trả về cho bạn kết quả trong chức năng kết xuất. mã sẽ trông như thế này:

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

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

}


-1

Một chút cập nhật với ES6các lớp thay vì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>
    );
  }
}

Ngoài ra - kiểm tra các phương thức vòng đời thành phần React: Vòng đời thành phần

Mọi thành phần đều có rất nhiều phương thức tương tự componentDidMountnhư vd.

  • componentWillUnmount() - thành phần sắp bị xóa khỏi trình duyệt DOM

1
Không thiếu tôn trọng, nhưng làm thế nào điều này trả lời câu hỏi? Hiển thị bản cập nhật trên ES6 không thực sự liên quan đến câu hỏi / không thay đổi bất cứ điều gì. Tất cả các câu trả lời cũ hơn đã nói về cách thành phầnDidMount không tự hoạt động.
dave4jr
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.