React Hook คำเตือนสำหรับฟังก์ชัน async ที่ใช้งานผล: ฟังก์ชัน useEffect ต้องส่งคืนฟังก์ชันล้างข้อมูลหรือไม่มีอะไรเลย


155

ฉันกำลังลองuseEffectตัวอย่างด้านล่าง:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

และฉันได้รับคำเตือนนี้ในคอนโซลของฉัน แต่การล้างข้อมูลเป็นทางเลือกสำหรับการเรียก async ที่ฉันคิด ฉันไม่แน่ใจว่าทำไมฉันถึงได้รับคำเตือนนี้ ตัวอย่างการเชื่อมโยงแซนด์บ็อกซ์ https://codesandbox.io/s/24rj871r0p ป้อนคำอธิบายภาพที่นี่

คำตอบ:


222

ฉันขอแนะนำให้ดูคำตอบของ Dan Abramov (หนึ่งในผู้ดูแลหลักของ React) ที่นี่ :

ฉันคิดว่าคุณกำลังทำให้มันซับซ้อนกว่าที่ควรจะเป็น

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

ในระยะยาวเราจะไม่สนับสนุนรูปแบบนี้เพราะมันส่งเสริมสภาพการแข่งขัน เช่น - อาจมีอะไรเกิดขึ้นระหว่างการโทรของคุณและสิ้นสุดและคุณอาจมีอุปกรณ์ประกอบฉากใหม่ เราขอแนะนำ Suspense สำหรับการดึงข้อมูลซึ่งจะมีลักษณะดังนี้

const response = MyAPIResource.read();

และไม่มีผลกระทบ แต่ในระหว่างนี้คุณสามารถย้ายข้อมูล async ไปยังฟังก์ชันแยกต่างหากและเรียกใช้

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับใจจดใจจ่อทดลองที่นี่


หากคุณต้องการใช้ฟังก์ชันภายนอกกับ eslint

 function OutsideUsageExample() {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }, [])

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

ด้วย useCallback useCallback . แซนด์บ็อกซ์

import React, { useState, useEffect, useCallback } from "react";

export default function App() {
  const [counter, setCounter] = useState(1);

  // if counter is changed, than fn will be updated with new counter value
  const fn = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  // if counter is changed, than fn will not be updated and counter will be always 1 inside fn
  /*const fnBad = useCallback(() => {
      setCounter(counter + 1);
    }, []);*/

  // if fn or counter is changed, than useEffect will rerun
  useEffect(() => {
    if (!(counter % 2)) return; // this will stop the loop if counter is not even

    fn();
  }, [fn, counter]);

  // this will be infinite loop because fn is always changing with new counter value
  /*useEffect(() => {
    fn();
  }, [fn]);*/

  return (
    <div>
      <div>Counter is {counter}</div>
      <button onClick={fn}>add +1 count</button>
    </div>
  );
}

คุณสามารถแก้ปัญหาสภาพการแข่งขันได้โดยตรวจสอบว่าส่วนประกอบนั้นไม่ได้ต่อเชื่อมหรือไม่: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard

2
นอกจากนี้คุณยังสามารถใช้แพคเกจชื่อใช้ async ผลกระทบ แพคเกจนี้ช่วยให้คุณสามารถใช้ไวยากรณ์ async await ได้
KittyCat

การใช้ฟังก์ชันเรียกใช้งานด้วยตนเองอย่าให้ async รั่วไหลไปยังนิยามฟังก์ชัน useEffect หรือการใช้ฟังก์ชันแบบกำหนดเองที่ทริกเกอร์การเรียก async เป็น wrapper รอบ useEffect เป็นทางออกที่ดีที่สุดสำหรับตอนนี้ ในขณะที่คุณสามารถรวมแพ็คเกจใหม่เช่น use-async-effect ที่แนะนำฉันคิดว่านี่เป็นปัญหาง่ายๆในการแก้ไข
Thulani Chivandikwa

2
เฮ้ไม่เป็นไรและสิ่งที่ฉันทำเกือบตลอดเวลา แต่eslintขอให้ฉันfetchMyAPI()เป็นที่พึ่งของuseEffect
Prakash Reddy Potlapadu

55

เมื่อคุณใช้ฟังก์ชัน async เช่น

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

จะส่งคืนคำสัญญาและuseEffectไม่คาดหวังว่าฟังก์ชันเรียกกลับจะส่งคืน Promise แต่คาดว่าจะไม่มีการส่งคืนหรือส่งคืนฟังก์ชัน

เพื่อเป็นวิธีแก้ปัญหาสำหรับคำเตือนคุณสามารถใช้ฟังก์ชัน async ที่เรียกด้วยตนเอง

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

หรือเพื่อให้สะอาดยิ่งขึ้นคุณสามารถกำหนดฟังก์ชันแล้วเรียกมัน

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

วิธีที่สองจะช่วยให้อ่านง่ายขึ้นและจะช่วยให้คุณเขียนโค้ดเพื่อยกเลิกคำขอก่อนหน้าหากมีการเรียกใช้คำขอใหม่หรือบันทึกการตอบกลับคำขอล่าสุดในสถานะ

รหัสการทำงาน


มีการทำแพ็คเกจเพื่อให้ง่ายขึ้น คุณสามารถค้นหาได้ที่นี่
KittyCat

1
แต่ eslint จะไม่ยอมกับสิ่งนั้น
Muhaimin CS

1
ไม่มีวิธีดำเนินการล้างข้อมูล / ไม่ได้โทรกลับ
David Rearte

1
@ShubhamKhatri เมื่อคุณใช้useEffectคุณสามารถส่งคืนฟังก์ชันเพื่อทำการล้างข้อมูลเช่นยกเลิกการสมัครรับกิจกรรม เมื่อคุณใช้ฟังก์ชัน async คุณจะไม่สามารถส่งคืนอะไรได้เลยเพราะuseEffectจะไม่รอผล
David Rearte

2
คุณกำลังบอกว่าฉันสามารถใส่ฟังก์ชันล้างข้อมูลใน async ได้หรือไม่? ฉันลองแล้ว แต่ฟังก์ชั่นการล้างข้อมูลของฉันไม่เคยถูกเรียกเลย ช่วยทำเป็นตัวอย่างหน่อยได้ไหม?
David Rearte

36

จนกว่า React จะให้วิธีที่ดีกว่าคุณสามารถสร้างตัวช่วยuseEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

ตอนนี้คุณสามารถส่งผ่านฟังก์ชัน async:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

อัปเดต

หากคุณเลือกแนวทางนี้โปรดทราบว่าเป็นรูปแบบที่ไม่ดี ฉันใช้วิธีนี้เมื่อฉันรู้ว่ามันปลอดภัย แต่มันก็เป็นรูปแบบที่ไม่ดีและจับจดอยู่เสมอ

ใจจดใจจ่อสำหรับการดึงข้อมูลซึ่งยังอยู่ระหว่างการทดลองจะช่วยแก้ปัญหาบางส่วนได้

ในกรณีอื่น ๆ คุณสามารถจำลองผลลัพธ์ async เป็นเหตุการณ์เพื่อให้คุณสามารถเพิ่มหรือลบ Listener ตามวงจรชีวิตของส่วนประกอบได้

หรือคุณสามารถจำลองผลลัพธ์ async เป็นObservableเพื่อที่คุณจะสามารถสมัครและยกเลิกการสมัครตามวงจรชีวิตของส่วนประกอบ


9
เหตุผลที่ React ไม่อนุญาตให้ใช้ฟังก์ชัน async โดยอัตโนมัติผลกระทบคือในกรณีส่วนใหญ่จำเป็นต้องมีการล้างข้อมูลบางอย่าง ฟังก์ชันuseAsyncEffectตามที่คุณเขียนไว้อาจทำให้ใครบางคนเข้าใจผิดได้อย่างง่ายดายหากพวกเขาส่งคืนฟังก์ชันล้างข้อมูลจากเอฟเฟกต์ async ของพวกเขามันจะทำงานในเวลาที่เหมาะสม สิ่งนี้อาจนำไปสู่การรั่วไหลของหน่วยความจำหรือข้อบกพร่องที่แย่ลงดังนั้นเราจึงเลือกที่จะสนับสนุนให้ผู้ใช้ refactor โค้ดของพวกเขาเพื่อทำให้ "รอยต่อ" ของฟังก์ชัน async ที่โต้ตอบกับวงจรชีวิตของ React มองเห็นได้ชัดเจนขึ้นและผลการดำเนินงานของโค้ดหวังว่าจะมีความรอบคอบและถูกต้องมากขึ้น
Sophie Alpert

9

ฉันอ่านคำถามนี้และรู้สึกว่าวิธีที่ดีที่สุดในการนำ useEffect ไม่ได้กล่าวถึงในคำตอบ สมมติว่าคุณมีการโทรผ่านเครือข่ายและต้องการทำอะไรบางอย่างเมื่อคุณได้รับคำตอบ เพื่อความง่ายเรามาเก็บการตอบสนองของเครือข่ายในตัวแปรสถานะ อาจต้องการใช้การดำเนินการ / ตัวลดเพื่ออัปเดตร้านค้าด้วยการตอบสนองของเครือข่าย

const [data, setData] = useState(null);

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

อ้างอิง => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects


6

สามารถใช้ตัวดำเนินการโมฆะได้ที่นี่
แทน:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

หรือ

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

คุณสามารถเขียน:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

มันดูสะอาดและน่ารักกว่าเล็กน้อย


เอฟเฟกต์ Async อาจทำให้หน่วยความจำรั่วได้ดังนั้นจึงเป็นเรื่องสำคัญที่จะต้องทำการล้างข้อมูลในการยกเลิกการต่อเชื่อมส่วนประกอบ ในกรณีของการดึงข้อมูลอาจมีลักษณะดังนี้:

function App() {
  const [data, setData] = React.useState([])

  React.useEffect(() => {
    const abortController = new AbortController()
    ;(async function fetchData() {
      try {
        const url = "https://jsonplaceholder.typicode.com/todos/1"
        const response = await fetch(url, {
          signal: abortController.signal,
        })
        setData(await response.json())
      } catch (error) {
        console.log("error", error)
      }
    })()
    return abortController.abort // cancel pending fetch request on component unmount
  }, [])

  return <pre>{JSON.stringify(data, null, 2)}</pre>
}


คุณรู้จัก JS ของคุณครับ AbortController เป็นสิ่งใหม่ที่มีอยู่หลังจากข้อเสนอสัญญาที่ยกเลิกไม่ได้
Devin G Rhode

BTW มีแพ็คเกจ "use-abortable-fetch" อยู่ที่นั่น แต่ฉันไม่แน่ใจว่าฉันชอบวิธีการเขียน มันจะเป็นการดีที่จะได้รับโค้ดรุ่นธรรมดาที่คุณมีอยู่ที่นี่เป็นตะขอที่กำหนดเอง นอกจากนี้ "รอที่นี่" เป็นแพ็คเกจที่ดีงามที่สามารถบรรเทาความจำเป็นในการลอง / จับบล็อกได้
Devin G Rhode

5

สำหรับผู้อ่านรายอื่นข้อผิดพลาดอาจมาจากการที่ไม่มีวงเล็บปิดฟังก์ชัน async:

พิจารณาฟังก์ชัน async initData

  async function initData() {
  }

รหัสนี้จะนำไปสู่ข้อผิดพลาดของคุณ:

  useEffect(() => initData(), []);

แต่อันนี้จะไม่:

  useEffect(() => { initData(); }, []);

(สังเกตวงเล็บรอบ initData ()


1
ยอดเยี่ยมผู้ชาย! ฉันใช้เทพนิยายและข้อผิดพลาดนั้นปรากฏขึ้นเมื่อฉันเรียกผู้สร้างการกระทำซึ่งส่งคืนวัตถุเพียงชิ้นเดียว ดูเหมือนว่า useEffect ฟังก์ชั่นเรียกกลับจะไม่เลียพฤติกรรมนี้ ฉันขอขอบคุณสำหรับคำตอบของคุณ
Gorr1995

3
ในกรณีที่ผู้คนสงสัยว่าเหตุใดจึงเป็นจริง ... หากไม่มีวงเล็บปีกกาค่าส่งคืนของ initData () จะถูกส่งกลับโดยปริยายโดยฟังก์ชันลูกศร ด้วยการจัดฟันแบบลอนจะไม่มีการส่งคืนโดยปริยายดังนั้นข้อผิดพลาดจะไม่เกิดขึ้น
Marnix.hoh

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.