簡體   English   中英

React useEffect 清理函數意外調用

[英]React useEffect cleanup function unexpectedly called

我正在創建一個自定義鈎子來在表單提交時獲取 api,我在 useEffect 鈎子內進行 api 調用,並且我有一個減速器來處理鈎子的狀態。 其中一個狀態是首先將trigger設置為 false 以控制 useEffect 是否執行任何操作,關鍵是鈎子返回一個函數,該函數會翻轉trigger值,該函數僅在您調用此函數時觸發 useEffect。 問題是在 api 調用期間調用了 useEffect 的清理函數,即使該組件顯然仍處於掛載狀態。

清理功能似乎因為我設置的值被解雇trigger基於其之前的值,當我設置trigger為固定值的清理功能不叫,但我失去了我的功能

const fetchReducer = (state, action) => {
    switch (action.type) {
        case 'FETCH_TRIGGER':
            return {
                ...state,
                trigger: !state.trigger
            }
        case 'FETCH_INIT':
            return {
                ...state,
                isLoading: true,
                isError: false
            };
        case 'FETCH_SUCCESS':
            return {
                ...state,
                isLoading: false,
                isError: false,
                datas: action.payload,
            };
        case 'FETCH_FAILURE':
            return {
                ...state,
                isLoading: false,
                isError: true,
            };
        default:
            throw new Error();
    }
}

const useFetchApi = (query, initialData = []) => {
    let isCancelled = false;
    const [state, dispatch] = useReducer(fetchReducer, {
        isLoading: false,
        isError: false,
        datas: initialData,
        trigger: false
    });
    const triggerFetch = _ => dispatch({ type: 'FETCH_TRIGGER' });
    const cancel = _ => { console.log("canceling");isCancelled = true };

    useEffect(_ => {
        if (!state.trigger)
            return;
        triggerFetch();
        (async _ => {
            dispatch({ type: 'FETCH_INIT' });
            try {
                const datas = await query();
                if (!isCancelled) { //isCancelled is true at this point
                    dispatch({ type: 'FETCH_SUCCESS', payload: datas })
                }
            } catch (err) {
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_FAILURE', payload: err })
                }
            }
        })();
        return cancel;
    }, [state.trigger]);
    return { ...state, triggerFetch};
}

用法:

function MyComponent () {
    const { datas, isLoading, isError, triggerFetch } = useFetchApi(query);
    return (
        <form onSubmit={event => {event.preventDefault(); triggerFetch()}}>
...

可以在 useEffect 回調中使用局部變量。 歸功於@gaearon

https://codesandbox.io/s/k0lm13kwxo

  useEffect(() => {
    let ignore = false;

    async function fetchData() {
      const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
      if (!ignore) setData(result.data);
    }

    fetchData();
    return () => { ignore = true; }
  }, [query]);

Tom Finney 從評論中給出的解決方案:

You could add another use effect that didn't do anything except for return that cancel function and have it with an empty array dependency that would mimic componentWillUnmount like useEffect(() => cancel, [])

const useFetchApi = (query, initialData = []) => {
    let isCancelled = false;
    const [state, dispatch] = useReducer(fetchReducer, {
        isLoading: false,
        isError: false,
        datas: initialData,
        trigger: false
    });
    const triggerFetch = _ => dispatch({ type: 'FETCH_TRIGGER' });
    const cancel = _ => { console.log("canceling");isCancelled = true };

    useEffect(_ => {
        if (!state.trigger)
            return;
        triggerFetch();
        (async _ => {
            dispatch({ type: 'FETCH_INIT' });
            try {
                const datas = await query();
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_SUCCESS', payload: datas })
                }
            } catch (err) {
                if (!isCancelled) {
                    dispatch({ type: 'FETCH_FAILURE', payload: err })
                }
            }
        })();
    }, [state.trigger]);
    useEffect(_=> cancel, []) //remove return cancel from useEffect and replace by this

    return { ...state, triggerFetch};
}

當前,您正在觸發狀態更改,這將觸發重新渲染,這將觸發一個效果,然后調用您的 API。 您真正想做的就是調用您的 API。

在下面的示例代碼中,我將triggerFetch更改為實際執行查詢並刪除了trigger狀態。 我添加了一個沒有依賴項的效果,以允許在卸載時取消。 我還更改了取消方法以使用 ref 而不是局部變量,以便它在重新渲染時保持不變。

import { useReducer, useEffect, useRef } from "react";

const fetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false
      };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        datas: action.payload
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true
      };
    default:
      throw new Error();
  }
};

const useFetchApi = (query, initialData = []) => {
  const cancelledRef = useRef(false);
  const [state, dispatch] = useReducer(fetchReducer, {
    isLoading: false,
    isError: false,
    datas: initialData,
    trigger: false
  });
  const triggerFetch = async _ => {
    dispatch({ type: "FETCH_INIT" });
    try {
      const datas = await query();
      if (!cancelledRef.current) {
        dispatch({ type: "FETCH_SUCCESS", payload: datas });
      }
    } catch (err) {
      if (!cancelledRef.current) {
        dispatch({ type: "FETCH_FAILURE", payload: err });
      }
    }
  };

  useEffect(_ => {
    return _ => {
      console.log("canceling");
      cancelledRef.current = true;
    };
  }, []);
  return { ...state, triggerFetch };
};

export default useFetchApi;

要記住的 2 件事:

  1. 每次 useEffect 運行時都會運行清理回調; 不包括組件的mount包括組件的unmount 如果要模擬 componentWillUnMount,請使用帶有空依賴項數組的 useEffect。
  2. 清理回調在 useEffect 中的其余代碼之前運行。 對於具有依賴項的 useEffect 記住這一點很重要,因為當任何依賴項發生變化並且清理回調和效果中的其余代碼都運行時,它將被調用。 這是 componentDidUpdate 行為。

如果你問我,清理這個名字是有誤導性的。 回調實際上在 componentDidUpdate 場景中的其余代碼之前運行,因此在這方面它不像准備那樣清理。 標題清理僅在卸載時才值得。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM