ค้นหาเว็บไซต์

reactjs - ฉันควรใช้ memoization หรือ callback เมื่อส่งฟังก์ชันเป็นพารามิเตอร์ของ custom hook?


ฉันเขียนฮุกแบบกำหนดเองที่รับฟังก์ชันแบบอะซิงค์เป็นพารามิเตอร์

ฮุกที่กำหนดเองจะเรียกฟังก์ชันและคืนค่าผลลัพธ์เมื่อเสร็จสิ้น

ฉันควรจะใช้ memoize หรือ useCallback เมื่อส่งฟังก์ชันไปหรือไม่?

นี่คือตัวอย่างการใช้งานฮุกของฉัน

    const {
        status,
        error,
        fire,
        data
    } = useAsyncOperation(
        () => {
            return Promise.resolve("Async result");
        },
        { autofire: true }
    );

สิ่งที่ฉันกลัวคือฟังก์ชันนิรนามแบบ inline จะเปลี่ยนไปในแต่ละครั้งที่เรนเดอร์ และว่า hook จะพิจารณามันเป็นค่าที่ใหม่ (แม้ว่าฟังก์ชันจะเหมือนเดิม)

ฉันควรจัดการเรื่องนี้อย่างไร?

สำหรับข้อมูลของคุณ: ฉันได้ตั้งค่าแซนด์บ็อกซ์โค้ดเพื่อแสดงการใช้งานฮุคที่ปรับแต่งเองของฉัน

และโค้ดของฮุคของฉันคือ:

import { useCallback, useEffect, useState } from 'react';

type UseAsyncOperationResult<TResult> = {
    status: 'idle' | 'pending' | 'success' | 'error';
    fire: () => Promise<TResult>;
    error?: Error;
    data?: TResult;
};

type UseAsyncOperationOptions<TResult> = {
    onSuccess?: (result: TResult) => void;
    onError?: (error: Error) => void;
    autofire?: boolean;
};

export const useAsyncOperation = <TResult>(
    operation: () => Promise<TResult>,
    options: UseAsyncOperationOptions<TResult> = {}
): UseAsyncOperationResult<TResult> => {
    // TODO: use reducer to manage state in a cleaner way
    const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
    const [error, setError] = useState<Error | undefined>();
    const [data, setData] = useState<TResult | undefined>();
    const [autofired, setAutofired] = useState(false);

    const { onSuccess, onError, autofire } = options;

    const fire = useCallback(async () => {
        setStatus('pending');
        try {
            const result = await operation();
            setStatus('success');
            setData(result);
            if (onSuccess) onSuccess(result);
            return result;
        } catch (error) {
            setStatus('error');
            setError(error as Error);
            if (onError) onError(error as Error);
            return Promise.reject(error);
        }
    }, [onError, onSuccess, operation]);

    useEffect(() => {
        if (autofire && !autofired) {
            console.log('autofire');
            setAutofired(true);
            void fire();
        }
    }, [autofire, fire, autofired]);

    return {
        status,
        fire,
        error,
        data,
    };
};

คำตอบ #1

ใช่ คุณจำเป็นต้องเก็บการอ้างอิงของ callback ที่คุณส่งไปยัง useAsyncOperation

วิธีในการกำหนดว่าเมื่อใดคุณต้องใช้ useCallback สำหรับฟังก์ชันหรือไม่คือการตรวจสอบว่าฟังก์ชันนั้นถูกติดตามเป็น dependency หรือไม่ และ custom hook ของคุณติดตาม operation ใน useCallback และผลลัพธ์ก็ถูกติดตามใน useEffect ด้วยเช่นกัน

ถ้าคุณไม่เก็บการอ้างอิงของ callback operation ของคุณ fire จะได้รับการอ้างอิงใหม่ในแต่ละการเรนเดอร์ เพราะมันติดตาม operation และนี่จะทำให้เกิดการเรียกใช้ useEffect ด้วยเช่นกัน เพราะมันติดตาม fire

คำตอบ #2

ทุกครั้งที่ operation เปลี่ยนแปลง (เช่น - ฟังก์ชันลูกศรจะเปลี่ยนแปลงในแต่ละการเรนเดอร์) useCallback จะเปลี่ยนแปลงและทำให้ useEffect ถูกเรียกใช้งาน อย่างไรก็ตาม การเรียกใช้ useEffect นั้นไม่เป็นไร เพราะหลังจากการเรียกครั้งแรก ตัวแปร autofired จะป้องกันการเรียกใช้งานครั้งอื่น ๆ

อย่างไรก็ตาม ถ้าคุณยังต้องการหลีกเลี่ยงการสร้างฟังก์ชัน fire แบบ memoized ซ้ำ และคุณไม่ต้องการ (และไม่สามารถ) บังคับให้ผู้ใช้ต้อง memoize operation เสมอ (ผ่านทาง useCallback) ให้ใช้ ref เพื่อเก็บ operation:

const opRef = useRef(operation);

// update opRef on each render
useEffect(() => {
  opRef.current = operation;
})

const fire = useCallback(async() => {
  setStatus('pending');
  try {
    const result = await opRef.current(); // call the function that is stored in the ref
    setStatus('success');
    setData(result);
    if (onSuccess) onSuccess(result);
    return result;
  } catch (error) {
    setStatus('error');
    setError(error as Error);
    if (onError) onError(error as Error);
    return Promise.reject(error);
  }
}, [onError, onSuccess]); // remove operation