Việc sử dụng async thành phầnDidMount () có tốt không?


139

Việc sử dụng componentDidMount()như một chức năng không đồng bộ thực hành tốt trong React Native hay tôi nên tránh nó?

Tôi cần nhận được một số thông tin từ AsyncStoragekhi thành phần gắn kết, nhưng cách duy nhất tôi biết để làm cho điều đó có thể là làm cho componentDidMount()chức năng không đồng bộ.

async componentDidMount() {
    let auth = await this.getAuth();
    if (auth) 
        this.checkAuth(auth);
}

Có bất kỳ vấn đề với điều đó và có bất kỳ giải pháp khác cho vấn đề này?


1
"Thực hành tốt" là một vấn đề quan điểm. Nó có hoạt động không? Đúng.
Kraylog

2
Đây là một bài viết hay cho thấy tại sao async await là một lựa chọn tốt so với lời hứa hackernoon.com/ Kẻ
Shubham Khatri

chỉ cần sử dụng redux-thunk nó sẽ giải quyết vấn đề
Tilak Maddy

@TilakMaddy Tại sao bạn cho rằng mọi ứng dụng phản ứng đều sử dụng redux?
Mirakurun

@Mirakurun tại sao toàn bộ stack stack cho rằng tôi sử dụng jQuery khi tôi thường hỏi các câu hỏi javascript đơn giản trong ngày?
Tilak Maddy

Câu trả lời:


162

Hãy bắt đầu bằng cách chỉ ra sự khác biệt và xác định cách nó có thể gây rắc rối.

Đây là mã của async và componentDidMount()phương thức vòng đời "đồng bộ hóa" :

// This is typescript code
componentDidMount(): void { /* do something */ }

async componentDidMount(): Promise<void> {
    /* do something */
    /* You can use "await" here */
}

Bằng cách xem mã, tôi có thể chỉ ra những khác biệt sau:

  1. Các asynctừ khóa: Trong bản thảo, đây chỉ là một mã đánh dấu. Nó làm 2 việc:
    • Buộc loại trả lại được Promise<void>thay vì void. Nếu bạn chỉ định rõ ràng loại trả về là không hứa hẹn (ví dụ: void), bản thảo sẽ gây ra lỗi cho bạn.
    • Cho phép bạn sử dụng awaitcác từ khóa bên trong phương thức.
  2. Kiểu trả về được thay đổi từ voidthànhPromise<void>
    • Nó có nghĩa là bây giờ bạn có thể làm điều này:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. Bây giờ bạn có thể sử dụng awaittừ khóa bên trong phương thức và tạm thời thực hiện nó. Như thế này:

    async componentDidMount(): Promise<void> {
        const users = await axios.get<string>("http://localhost:9001/users");
        const questions = await axios.get<string>("http://localhost:9001/questions");
    
        // Sleep for 10 seconds
        await new Promise(resolve => { setTimeout(resolve, 10000); });
    
        // This line of code will be executed after 10+ seconds
        this.setState({users, questions});
        return Promise.resolve();
    }

Bây giờ, làm thế nào họ có thể gây ra rắc rối?

  1. Các asynctừ khóa là hoàn toàn vô hại.
  2. Tôi không thể tưởng tượng bất kỳ tình huống nào trong đó bạn cần thực hiện một cuộc gọi đến componentDidMount()phương thức để kiểu trả về Promise<void>cũng vô hại.

    Gọi một phương thức có kiểu trả về Promise<void>không có awaittừ khóa sẽ không có gì khác biệt so với cách gọi kiểu có kiểu trả về void.

  3. Vì không có phương pháp vòng đời sau khi componentDidMount()trì hoãn thực hiện nên có vẻ khá an toàn. Nhưng có một gotcha.

    Giả sử, những điều trên this.setState({users, questions});sẽ được thực hiện sau 10 giây. Giữa thời gian trì hoãn, một ...

    this.setState({users: newerUsers, questions: newerQuestions});

    ... đã được thực hiện thành công và DOM đã được cập nhật. Kết quả đã được hiển thị cho người dùng. Đồng hồ tiếp tục tích tắc và 10 giây trôi qua. Việc trì hoãn this.setState(...)sau đó sẽ thực thi và DOM sẽ được cập nhật lại, thời điểm đó với người dùng cũ và các câu hỏi cũ. Kết quả cũng sẽ được hiển thị cho người dùng.

=> Nó khá an toàn (tôi không chắc chắn về 100%) để sử dụng asyncvới componentDidMount()phương thức. Tôi là một fan hâm mộ lớn của nó và cho đến nay tôi không gặp phải bất kỳ vấn đề nào khiến tôi quá đau đầu.


Khi bạn nói về vấn đề xảy ra khi một setState khác xảy ra trước một Promise đang chờ xử lý, điều đó có giống với Promise mà không có async / đang chờ đường cú pháp hay thậm chí là các cuộc gọi lại cổ điển không?
Clafou

3
Đúng! Trì hoãn a setState()luôn sở hữu một rủi ro nhỏ. Chúng ta nên tiến hành cẩn thận.
Cù Đức Từ

Tôi đoán một cách để tránh các vấn đề là sử dụng một cái gì đó như isFetching: truebên trong trạng thái của một thành phần. Tôi chỉ sử dụng điều này với redux nhưng tôi cho rằng nó hoàn toàn hợp lệ với quản lý nhà nước chỉ phản ứng. Mặc dù nó không thực sự giải quyết vấn đề của cùng một trạng thái được cập nhật ở một nơi khác trong mã ...
Clafou

1
Tôi đồng ý với điều đó. Trong thực tế, isFetchinggiải pháp cờ khá phổ biến đặc biệt là khi chúng tôi muốn phát một số hình ảnh động ở phía trước trong khi chờ phản hồi phía sau ( isFetching: true).
Cù Đức Từ

3
Bạn có thể gặp sự cố nếu bạn thực hiện setState sau khi thành phần không được kết nối
Eliezer Steinbock

18

Cập nhật tháng 4 năm 2020: Vấn đề dường như được khắc phục trong React 16.13.1 mới nhất, xem ví dụ về hộp cát này . Cảm ơn @abernier đã chỉ ra điều này.


Tôi đã thực hiện một số nghiên cứu và tôi đã tìm thấy một sự khác biệt quan trọng: React không xử lý lỗi từ các phương thức vòng đời không đồng bộ.

Vì vậy, nếu bạn viết một cái gì đó như thế này:

componentDidMount()
{
    throw new Error('I crashed!');
}

sau đó lỗi của bạn sẽ bị ràng buộc bởi lỗi liên kết và bạn có thể xử lý nó và hiển thị một thông báo duyên dáng.

Nếu chúng ta thay đổi mã như thế này:

async componentDidMount()
{
    throw new Error('I crashed!');
}

tương đương với điều này:

componentDidMount()
{
    return Promise.reject(new Error('I crashed!'));
}

sau đó lỗi của bạn sẽ được âm thầm nuốt . Thật xấu hổ cho bạn, Phản ứng ...

Vì vậy, làm thế nào để chúng tôi xử lý lỗi hơn? Cách duy nhất có vẻ là bắt rõ ràng như thế này:

async componentDidMount()
{
    try
    {
         await myAsyncFunction();
    }
    catch(error)
    {
        //...
    }
}

hoặc như thế này:

componentDidMount()
{
    myAsyncFunction()
    .catch(()=>
    {
        //...
    });
}

Nếu chúng tôi vẫn muốn lỗi của mình đạt đến ranh giới lỗi, tôi có thể nghĩ về thủ thuật sau:

  1. Bắt lỗi, làm cho trình xử lý lỗi thay đổi trạng thái thành phần
  2. Nếu trạng thái chỉ ra lỗi, hãy ném nó từ renderphương thức

Thí dụ:

class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  buggyAsyncfunction(){ return Promise.reject(new Error('I crashed async!'));}

  async componentDidMount() {
    try
    {
      await this.buggyAsyncfunction();
    }
    catch(error)
    {
        this.setState({error: error});
    }
  }

  render() {
    if(this.state.error)
        throw this.state.error;

    return <h1>I am OK</h1>;
  }
}

Có một vấn đề được báo cáo cho điều này? Có thể hữu ích để báo cáo nếu vẫn còn trường hợp ... thx
abernier

@abernier Tôi nghĩ rằng đó là do thiết kế ... Mặc dù có lẽ họ có thể cải thiện nó. Tôi đã không gửi bất kỳ vấn đề nào về việc này ...
CF

1
có vẻ như không còn như vậy nữa, ít nhất là với React 16.13.1 như đã được thử nghiệm ở đây: scriptsandbox.io/s/bold-ellis-n1cid?file=/src/App.js
abernier

9

Mã của bạn là tốt và rất dễ đọc với tôi. Xem bài viết này của Dale Jefferson, nơi ông cho thấy một componentDidMountví dụ không đồng bộ và trông cũng thực sự tốt.

Nhưng một số người sẽ nói rằng một người đọc mã có thể cho rằng React thực hiện điều gì đó với lời hứa đã trả lại.

Vì vậy, việc giải thích mã này và nếu nó là một thực hành tốt hay không là rất cá nhân.

Nếu bạn muốn một giải pháp khác, bạn có thể sử dụng lời hứa . Ví dụ:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}

3
... Hoặc cũng có thể, chỉ sử dụng một asynchàm nội tuyến với awaits bên trong ...?
Erik Kaplun

cũng là một lựa chọn @Erik ALLik :)
Tiago Alves

@Erik ALLik bạn có ví dụ không?
Pablo Rincon

1
@PabloRincon smth như (async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()ở đâu fetchsubmitRequestlà các hàm trả lại lời hứa.
Erik Kaplun

Mã này chắc chắn là xấu, bởi vì nó sẽ nuốt bất kỳ lỗi nào xảy ra trong hàm getAuth. Và nếu chức năng làm một cái gì đó với mạng (ví dụ), lỗi phải được dự kiến.
CF

6

Khi bạn sử dụng componentDidMountmà không có asynctừ khóa, tài liệu nói điều này:

Bạn có thể gọi setState () ngay lập tức trong thành phầnDidMount (). Nó sẽ kích hoạt kết xuất thêm, nhưng nó sẽ xảy ra trước khi trình duyệt cập nhật màn hình.

Nếu bạn sử dụng, async componentDidMountbạn sẽ mất khả năng này: một kết xuất khác sẽ xảy ra SAU khi trình duyệt cập nhật màn hình. Nhưng imo, nếu bạn đang suy nghĩ về việc sử dụng async, chẳng hạn như tìm nạp dữ liệu, bạn không thể tránh khỏi trình duyệt sẽ cập nhật màn hình hai lần. Ở một thế giới khác, không thể PAUSE thành phầnDidMount trước khi trình duyệt cập nhật màn hình


1
Tôi thích câu trả lời này vì nó ngắn gọn và được hỗ trợ bởi các tài liệu. Bạn có thể vui lòng thêm một liên kết đến các tài liệu bạn đang tham khảo.
theStherSide

Điều này thậm chí có thể là một điều tốt, ví dụ nếu bạn đang hiển thị trạng thái tải trong khi tài nguyên đang tải và sau đó là nội dung khi nó được thực hiện.
Hjulle

3

Cập nhật:

(Bản dựng của tôi: Phản ứng 16, Webpack 4, Babel 7):

Khi sử dụng Babel 7, bạn sẽ khám phá:

Sử dụng mẫu này ...

async componentDidMount() {
    try {
        const res = await fetch(config.discover.url);
        const data = await res.json();
        console.log(data);
    } catch(e) {
        console.error(e);
    }
}

bạn sẽ gặp phải lỗi sau ...

Uncaught ReferenceError: rebatorR.78 không được xác định

Trong trường hợp này, bạn sẽ cần cài đặt babel-plugin-Transform-runtime

https://babeljs.io/docs/en/babel-plugin-transform-r nb.html

Nếu vì lý do nào đó bạn không muốn cài đặt gói trên (babel-plugin-Transform-runtime) thì bạn sẽ muốn bám vào mẫu Promise ...

componentDidMount() {
    fetch(config.discover.url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })
    .catch(err => console.error(err));
}

3

Tôi nghĩ rằng nó tốt miễn là bạn biết những gì bạn đang làm. Nhưng nó có thể gây nhầm lẫn bởi vì async componentDidMount()vẫn có thể chạy sau khi componentWillUnmountđã chạy và thành phần này chưa được kết thúc.

Bạn cũng có thể muốn bắt đầu cả nhiệm vụ đồng bộ và không đồng bộ bên trong componentDidMount. Nếu không componentDidMountđồng bộ, bạn sẽ phải đặt tất cả mã đồng bộ trước mã đầu tiên await. Có thể không rõ ràng với ai đó rằng mã trước khi lần đầu tiên awaitchạy đồng bộ. Trong trường hợp này, tôi có thể sẽ giữ componentDidMountđồng bộ nhưng yêu cầu nó gọi các phương thức đồng bộ hóa và không đồng bộ.

Cho dù bạn chọn async componentDidMount()so với phương thức componentDidMount()gọi đồng bộ hóa async, bạn phải đảm bảo bạn dọn sạch mọi phương thức nghe hoặc phương thức không đồng bộ vẫn có thể đang chạy khi ngắt kết nối thành phần.


2

Trên thực tế, tải async trong ElementDidMount là một mẫu thiết kế được đề xuất khi React tránh xa các phương thức vòng đời kế thừa (thành phầnWillMount, thành phầnWillReceiveProps, thành phầnWillUpdate) và chuyển sang Async Rendering.

Bài đăng trên blog này rất hữu ích trong việc giải thích lý do tại sao điều này an toàn và cung cấp các ví dụ để tải async trong ElementDidMount:

https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html


3
Kết xuất Async thực sự không liên quan gì đến việc tạo vòng đời một cách rõ ràng không đồng bộ. Nó thực sự là một mô hình chống. Giải pháp được đề xuất là thực sự gọi một phương thức không đồng bộ từ phương pháp vòng đời
Clayton Ray

1

Tôi thích sử dụng một cái gì đó như thế này

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
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.