React useEffect清理函数意外调用

5
我正在创建一个自定义 hook,在表单提交时使用它来获取 api 数据,我在 useEffect hook 中进行 api 调用并使用 reducer 来处理 hook 的状态。其中一个状态是初始设置为 false 的 trigger,用于控制 useEffect 是否执行任何操作,问题是该 Hook 返回一个函数,该函数可以翻转 trigger 值,仅当您调用此函数时才会触发 useEffect。
问题在于,即使组件明显仍然挂载,useEffect 的清理函数也会在 api 调用期间被调用,这似乎是因为我根据其先前的值设置了 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()}}>
...

1
清理函数在每次 effect 再次运行以及卸载时都会被调用,在您的情况下,它将在每次 state.trigger 改变时运行。 - Tholle
2
如果你在 effect 的第二个参数传递一个空数组,那么从 effect 返回的清理函数只会在卸载时被显式调用。否则,每当依赖项数组中的任何内容发生更改时,清理函数都将被调用。 - Tom Finney
3
你可以添加另一个useEffect,除了返回cancel函数之外什么也不做,并且具有一个空数组依赖项,这将模仿componentWillUnmount,如useEffect(() => cancel, []) - Tom Finney
4个回答

4

本地变量可以在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]);

3

需要记住的两件事:

  1. 清理回调函数每次useEffect运行都会运行;这个不包括组件的挂载包括组件的卸载。如果你想模拟componentWillUnmount,使用一个空数组的useEffect。
  2. 清理回调函数在useEffect内部的其他代码之前运行。这对于具有依赖关系的useEffect非常重要,因为当任何一个依赖项更改时,它将被调用,同时清理回调和effect内部的其余代码都将运行。这就是componentDidUpdate的行为。

如果问我,称之为清理是具有误导性的。在componentDidUpdate情况下,回调实际上是在effect内部的其他代码之前运行的,因此在这方面它不是那么多的清理,而是准备工作。只有在卸载时才真正需要清理。


1

以下是Tom Finney在评论中提供的解决方案:

您可以添加另一个useEffect,除了返回取消函数外,什么也不做,并将其与空数组依赖项一起使用,这将模仿componentWillUnmount,例如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};
}

这真的有效吗?难道不应该是 useEffect(()=> () => cancel(), []) 吗? - Pavel Ye
@Gatoyu 我认为这个解决方案无法在卸载时成功取消。第二个 effect 中的 cancel 函数将始终是初始渲染中的 cancel,它将更改初始渲染中的 isCancelled 变量。调用 triggerFetch 将触发重新渲染,这意味着调用 query 的第一个 effect 将使用来自重新渲染的 isCancelled,而这个值不会被第二个 effect 更改。这就是为什么需要一个 ref 的原因。 - Ryan Cogswell

1

目前,您正在触发状态更改,这将触发重新渲染,然后触发效果,然后调用您的API。您真正想做的就是调用您的API。

在下面的示例代码中,我已将triggerFetch更改为实际执行查询并删除了trigger状态。我添加了一个没有依赖项的effect以允许在卸载时取消。我还更改了取消方法以使用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;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接